黑马程序员——Java学习笔记 多线程

时间:2023-02-20 09:58:37

------<a href="http://www.itheima.com" target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流! -------

一、线程的概述

    在多任务操作系统中,通过运行多个进程来并发地执行多个任务。由于每个线程都是一个独立执行自身指令的不同控制流,因此一个包含多线程的进程也能够实现进程内多项任务的并发执行。例如一个进程中可以包含三个线程,一个线程运行GUI,第二个线程执行I/O操作,第三那个线程执行后台计算工作。在但处理器的计算机上,多个线程实际上并不能并发执行,但系统可以按照某种调度策略在线程之间切换。线程的切换是有系统在气短的时间内完成的,所以给人的印象是并发执行。
    线程与进程在概念上是相关的。进程有代码、数据、内核状态和一组寄存器组成,而线程是有表示程序运行状态的寄存器(例如程序计数器、栈指针)以及堆栈组成。线程不包含进程地址空间中的代码和数据,线程是计算过程在某一时刻的状态。所以,系统在产生一个线程或各个线程之间的切换时,负担要比进程小得多,因此线程也被称为轻型进程。进程是一个内核级的实体,进程结构的所有成分都在内核空间中,一个用户程序不能直接访问这些数据。线程是一个用户级的实体,线程结构驻留在用户空间中,能够被普通的用户级函数直接访问。
    线程是程序中的一个执行流。一个执行流是由CUP运行程序代码并操纵程序的数据所形成的。新词,线程被认为是以CPU为主体的行为。在Java中线程的模型就是一个CPU、程序代码和数据的封装体。
Java中的线程模型包含三部分:
    1、一个虚拟的CPU。
    2、该CPU指向的代码。
    代码与数据是相互独立的,代码可以与其他线程共享,也可以不共享,当两个线程执行同一个类的示例代码时,他们共享相同的代码。
    3、代码所操作的数据。
    数据与代码是独立的。数据也可以被多个线程共享,当两个线程对同一个对象进行访问时,它们将共享数据。
    线程模型在Java中是由java.lang.Thread类进行定义和描述的。程序中的线程都是Thread类的实例。因此永和可以通过创建Thread的实例或定义并创建Thread子类的实例建立和控制自己的线程。
小结
    进程是一个正在执行中的程序。每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
    线程就是进程中的一个独立的控制单元。线程在控制着进程的执行。
    一个进程中至少有一个线程。
    Java VM启动的时候会有一个进程java.exe.该进程中至少一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。
    扩展:其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程

二、线程的创建

    Java.lang中的Thread类是多线程程序设计的基础。创建线程是通过调用Thread类的构造方法实现的。线程的创建中,线程体的构造是关键。任何实现Runnable接口的对象都可以作为Thread类构造方法中的target参数,而Thread类本身也实现了Runnable接口,因此可以有两种方式提供run()方法的实现:继承Thread类和实现Runnable接口。
1、创建线程的第一种方式:继承Thread类。
步骤:
    (1)定义类继承Thread。
    (2)复写Thread类中的run方法。
    目的:将自定义代码存储在run方法。让线程运行。
    (3)调用线程的start方法,
    该方法两个作用:启动线程,调用run方法。
为什么要覆盖run方法呢?
    Thread类用于描述线程。该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码。
练习:
    创建两个线程,和主线程交替运行。
    原来线程都有自己默认的名称。Thread-编号 该编号从0开始。
    static Thread currentThread():返回对当前正在执行的线程对象的引用。
    getName(): 返回该线程的名称。
    设置线程名称:setName或者构造函数

<span style="font-size:14px;">
class Test extends Thread //继承Thread类
{
Test(String name) //通过构造函数自定义线程名称。
{
super(name);
}
public void run() //重写run()方法。
{
for(int x=0; x<60; x++)
{
System.out.println((Thread.currentThread()==this)+"..."+this.getName()+" run..."+x);
}
}
}

class ThreadText
{
public static void main(String[] args)
{
Test t1 = new Test("one---"); //创建线程t1
Test t2 = new Test("two+++"); //创建线程t2
t1.start(); //启动线程t1
t2.start(); //启动线程t2

for(int x=0; x<60; x++) //主线程main
{
System.out.println("main....."+x);
}
}
}</span>
运行结果:
 黑马程序员——Java学习笔记 多线程
2、创建线程的第二种方式:实现Runable接口
步骤:
    (1)定义类实现Runnable接口
    (2)覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中。
    (2)通过Thread类建立线程对象。
    (4)将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
为什么要将Runnable接口的子类对象传递给Thread的构造函数?。
    因为,自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去指定指定对象的run方法。就必须明确该run方法所属对象。
    (5)调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
练习:
    需求:简单的卖票程序。
    多个窗口同时买票。
<span style="font-size:14px;">class Ticket implements Runnable
{
private int tick = 100;
public void run() //重写run()方法。
{
while(true)
{
if(tick>0)
{
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--); //显示线程名称和剩余票数。
}
}
}
}

class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket(); //创建Runnable接口的子类对象。

Thread t1 = new Thread(t); //创建线程t1
Thread t2 = new Thread(t); //创建线程t2
Thread t3 = new Thread(t); //创建线程t3
Thread t4 = new Thread(t); //创建线程t4
t1.start();
t2.start();
t3.start();
t4.start();
}
}</span>
运行结果:
 黑马程序员——Java学习笔记 多线程
3、创建线程两种方法的比较
    实现方式和继承方式有什么区别呢?
    实现方式好处:避免了单继承的局限性。
    在定义线程时,建立使用实现方式。
    两种方式区别:
    继承Thread:线程代码存放Thread子类run方法中。
    实现Runnable,线程代码存在接口的子类的run方法。

三、多线程的安全问题

1、导致安全问题的出现的原因:
    在多线程的程序中,当多个线程并发执行时,虽然各个线程中的语句的执行顺序是确定的,但线程的相对执行顺序是不确定的。有些情况下,例如多线程对共享数据操作时,这种线程运行顺序的不确定性将会产生执行结果的不确定性,使共享数据的一致性被破坏,因此在某些应用程序中必须对线程的并发操作进行控制。
2、线程同步
    Java中对共享数据操作的并发控制是采用传统的*技术。一个程序的各个并发线程对同一个对象进行访问的代码段,称为临界区(critical section)。在Java语言中,临界区可以是一个一个语句块或是一个方法,并且用synchronized关键词标识。
    临界区的控制是通过对象锁进行的。Java平台将每个由synchronized(someObject){}语句指定的someObject设置一个锁,称为对象锁(monitor)。对象锁是一种独占的排它锁(exclusive locks)。
    Java对于多线程的安全问题提供了专业的解决方式。就是同步代码块。
synchronized(对象)
{
    需要被同步的代码
}
    对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
同步的前提:
    (1)必须要有两个或者两个以上的线程。
    (2)必须是多个线程使用同一个锁。
    必须保证同步中只能有一个线程在运行。
    好处:解决了多线程的安全问题。
    弊端:多个线程需要判断锁,较为消耗资源。
3、同步函数用的是哪一个锁呢?
    函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this。
4、如果同步函数被静态修饰后,使用的锁是什么呢?

    通过验证,发现不在是this。因为静态方法中也不可以定义this。静态进内存是,内存中没有本类对象,但是一定有该类对应的字节码文件对象。类名.class。该对象的类型是Class。静态的同步方法,使用的锁是该方法所在类的字节码文件对象。类名.class。

    静态同步函数示例:public static synchronized void show(){}

5、练习:单例设计模式。
饿汉式:
<span style="font-size:14px;">
class Single
{            //创建本类的私有静态成员对象s,并有final修饰
    private static final Single s = new Single();
                 //定义私有构造函数,禁止其他类创建新到对象
private Single(){}
                //定义静态方法,调用对象s。
public static Single getInstance()
    {
        return s;
    }
}</span>
懒汉式:
<span style="font-size:14px;">
class Single
{
        //创建本类的私有静态成员对象s,但不开辟内存。
private static Single s = null;
        //定义私有构造函数,禁止其他类创建新到对象
private Single(){}
        //定义静态方法,为对象开辟空间并调用s
public static  Single getInstance()
    {
        if(s==null)
        {
            synchronized(Single.class)
            {
                if(s==null)
                    s = new Single();
            }
        }
        return s;
    }
}</span>

6、死锁
    如果程序中多个线程互相等待对方持有的锁,而在得到对方锁之前都不会释放自己的锁,由此导致这些线程不能继续运行,这就是死锁。
示例:

<span style="font-size:14px;">
class LockTest implements Runnable
{
    private boolean flag;
    LockTest(boolean flag)        //构造函数,传入值boolean型flag
    {
        this.flag = flag;
    }

    public void run()
    {
        if(flag)        //唤醒条件
        {
            while(true)
            {
                synchronized(MyLock.locka)      //locka锁中嵌套lockb锁。
                      {
    System.out.println(Thread.currentThread().getName()+"...if locka ");
                    synchronized(MyLock.lockb)
                    {
                                          System.out.println(Thread.currentThread().getName()+"..if lockb");    
                    }
                }
            }
        }
        else
        {
            while(true)
            {
                synchronized(MyLock.lockb)         //lockb锁中嵌套locka锁。
                {
    System.out.println(Thread.currentThread().getName()+"..else lockb");
                    synchronized(MyLock.locka)
                    {
    System.out.println(Thread.currentThread().getName()+".....else locka");
                    }
                }
            }
        }
    }
}

class MyLock           //定义locka锁和lockb锁。
{
    static Object locka = new Object();
    static Object lockb = new Object();
}

class  DeadLockTest
{
    public static void main(String[] args)
    {
        Thread t1 = new Thread(new LockTest(true));    //创建线程t1
        Thread t2 = new Thread(new LockTest(false));    //创建线程t2
        t1.start();    //启动线程
        t2.start();
    }
}</span>

四、线程间通讯

1、线程间通讯的概念
    其实就是多个线程在操作同一个资源,但是操作的动作不同。
2、等待唤醒
    有时,当某个线程进入synchronized块后,共享数据的状态并不满足它的需要,它要等待其他线程将共享数据改变为它需要的状态后才能继续执行。但由于此时它占有了该对象的锁,其他线程无法对共享数据进行操作。为此Java引入wait()和notify()。这两个方法是Java.Lang.object类的方法,是实现线程通信的两个方法。
    wait()和notify()都使用在同步中,因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁。
3、为什么这些操作线程的方法要定义Object类中呢?
    因为这些方法在操作同步中线程时,都必须要标识它们所操作线程只有的锁,只有同一个锁上的被等待线程,可以被同一个锁上notify唤醒。不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法定义Object类中。
4、等待唤醒机制示例
<span style="font-size:14px;">
class Res
{
private String name;
private String sex;
private boolean flag = false;

public synchronized void set(String name,String sex)//同步函数输入
{
if(flag)//判断标记
try{this.wait();}catch(Exception e){}//等待

this.name = name;
this.sex = sex;

flag = true;//更改标记
this.notify();//唤醒
}

public synchronized void out()//同步函数输出
{
if(!flag) //判断标记
try{this.wait();}catch(Exception e){}

System.out.println(name+"........"+sex);
flag = false;
this.notify();
}
}

class Input implements Runnable//定义类实现Runnable接口
{
private Res r ;
Input(Res r) //构造函数
{
this.r = r;
}
public void run()//重写run方法
{
int x = 0;
while(true)//给对象循环赋值
{
if(x==0)
r.set("小明","男");
else
r.set("Rose,"women");
x = (x+1)%2;
}
}
}

class Output implements Runnable
{
private Res r ;

Output(Res r)
{
this.r = r;
}
public void run()
{
while(true)//循环输出
{
r.out();
}
}
}

class InputOutputDemo2
{
public static void main(String[] args)
{
Res r = new Res();

new Thread(new Input(r)).start();//创建并开启线程
new Thread(new Output(r)).start();
}
}</span>
运行结果:

黑马程序员——Java学习笔记 多线程

5、生产者消费者
    JDK1.5 中提供了多线程升级解决方案。将同步Synchronized替换成现实Lock操作。将Object中的wait,notify notifyAll,替换了Condition对象。该对象可以Lock锁 进行获取。该示例中,实现了本方只唤醒对方操作。
(1)Lock:替代了Synchronized
    lock unlock newCondition()
(2)Condition:替代了Object wait notify notifyAll
    await();signal();signalAll();
示例:

<span style="font-size:14px;">
import java.util.concurrent.locks.*;//导入包

class ProducerConsumerDemo2
{
public static void main(String[] args)
{
Resource r = new Resource(); //商品对象

Producer pro = new Producer(r); //生产者对象
Consumer con = new Consumer(r); //消费者对象

Thread t1 = new Thread(pro); //创建线程
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);

t1.start(); //启动线程
t2.start();
t3.start();
t4.start();

}
}

class Resource
{
private String name;
private int count = 1; //产品编号
private boolean flag = false; //设置标记

private Lock lock = new ReentrantLock(); //创建锁

private Condition condition_pro = lock.newCondition(); //生产者的condition对象
private Condition condition_con = lock.newCondition(); /消费者的condition对象

public void set(String name)throws InterruptedException //生产者唤醒消费者线程
{
lock.lock(); //获取锁
try
{
while(flag)
condition_pro.await();//生产者线程等待

this.name = name+"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name); //输出生产信息
flag = true;
condition_con.signal(); //唤醒消费者线程
}
finally
{
lock.unlock(); //通过finally语句使释放锁的动作一定执行。
}
}

public void out()throws InterruptedException //消费者唤醒生产者线程
{
lock.lock();
try
{
while(!flag)
condition_con.await();
System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name); //输出消费信息
flag = false;
condition_pro.signal();
}
finally
{
lock.unlock();
}
}
}

class Producer implements Runnable //生产者线程
{
private Resource res;

Producer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
try
{
res.set("+商品+");
}
catch (InterruptedException e)
{
}
}
}
}

class Consumer implements Runnable //消费者线程
{
private Resource res;

Consumer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
try
{
res.out();
}
catch (InterruptedException e)
{
}
}
}
}</span>

运行结果:

黑马程序员——Java学习笔记 多线程

五、线程的基本控制

1、stop()方法 

    在JDK 1.5版本之前,有stop停止线程的方法,但升级之后,此方法已经过时。stop方法已经过时,如何停止线程?
    只有一种,run方法结束。开启多线程运行,运行代码通常是循环结构。只要控制住循环,就可以让run方法结束,也就是线程结束。即定义一个循环结束标记。
特殊情况:
    当线程处于了冻结状态。就不会读取到标记。那么线程就不会结束。当没有指定的方式让冻结的线程恢复到运行状态是,这时需要对冻结进行清除。强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。

2、interrupt()方法

    如果一个线程t在调用sleep()、join()、wait()等方法被阻塞时,则t.interrupt()方法将中断t的阻塞状态,并且将t接收到InterruptException异常。

3、setDaemon()方法

    public final void setDaemon(boolean on),设置当前线程为Daemon线程,该方法必须在线程启动前调用。Daemon可称为守护线程,当正在运行的线程都是守护线程时,Java 虚拟机退出。

4、join方法

        当A线程执行到了B线程的.join()方法时,A线程就会等待,等B线程都执行完,A线程才会执行。(此时B和其他线程交替运行。)join用来临时加入线程。

5.toString

    返回该线程的字符串表示形式,包括线程名称、优先级和线程组。

6、setPriority()方法用来设置优先级

        MAX_PRIORITY 最高优先级10

        MIN_PRIORITY   最低优先级1

        NORM_PRIORITY 分配给线程的默认优先级5

7、yield()

    方法可以暂停当前线程,让其他线程执行。稍微降低线程的执行频率,达到都有机会运行的状态。

六、多线程开放运用技巧

    通过匿名内部类实现多线程。

<span style="font-size:14px;">
class ThreadTest
{
public static void main(String[] args)
{

new Thread() //线程1,继承thread类
{
public void run()
{
for(int x=0; x<100; x++)
{
System.out.println(Thread.currentThread().getName()+"....."+x);
}
}
}.start();

for(int x=0; x<100; x++) //线程2,主线程
{
System.out.println(Thread.currentThread().getName()+"....."+x);
}

Runnable r = new Runnable() //线程3,实现Runnable接口
{
public void run()
{
for(int x=0; x<100; x++)
{
System.out.println(Thread.currentThread().getName()+"....."+x);
}
}
};
new Thread(r).start();
}

</span>