<jdk7学习笔记>读书笔记-线程

535 查看

Tread和Runnable

定义线程

  • 实现Runnable接口,重写run()方法

  • 继承Thread类,重写run()方法

启动线程

        Runnable tortoise=new Tortoise();
        Thread thread1=new Thread(tortoise);
        thread1.start();
        Thread thread1=new Tortoise();
        thread1.start();

线程生命周期

daemon线程

主线程会从main()方法开始执行,直到main()方法结束后停止jvm.如果主线程中启动了额外线程,则主线程默认会等到被启动的所有额外线程都执行完run()方法才会终止jvm.

public class Daemon implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("TheViper");
        }
    }
    public static void main(String[] args){
        Thread thread=new Thread(new Daemon());
        //thread.setDaemon(true);
        thread.start();
    }
}
TheViper
TheViper
TheViper
...

控制台会不停的打印。如果把注释去掉,thread.setDaemon(true),则在所以非daemon线程都结束时,jvm自动终止。
默认所有从daemon线程产生的线程也是daemon线程。

线程基本状态图

启动线程后,基本状态可分为可执行(runnable),阻塞(blocked),执行中(running).

在一个时间点上,一个cpu只能执行一个线程,只是cpu会不断切换线程。可以用Thread的setPriority()方法设置线程的优先级,设定值1-10,默认为5.优先级越高的线程,越有机会抢占cpu.
线程进入blocked状态可能的情况:

  • 调用Thread.sleep()方法

  • 进入synchronized前,对竞争对象的锁定

  • 调用wait()方法阻断

  • 等待io完成

  • ...

一个处于blocked状态的线程,可由另一个线程调用interrupt()方法,让它离开blocked状态。

public class Daemon implements Runnable{
    @Override
    public void run() {
        System.out.println("TheViper");
        try {
            Thread.sleep(3000);//进入blocked状态
        } catch (InterruptedException e) {
            System.out.println("离开blocked状态");
        }
    }
    public static void main(String[] args){
        Thread thread=new Thread(new Daemon());
        thread.start();
        thread.interrupt();
    }
}
TheViper
离开blocked状态

安插线程

假设线程A正在运行,这时线程B想占用cpu运行,等它运行完后,继续运行线程A.这种情况可以使用join()方法.
未使用join()

public class Daemon implements Runnable{
    @Override
    public void run() {
        System.out.println("线程开始");
        try {
            Thread.sleep(3000);
            System.out.println("线程执行");
        } catch (InterruptedException e) {
            System.out.println("离开blocked状态");
        }
        System.out.println("线程结束");
    }
    public static void main(String[] args){
        Thread thread=new Thread(new Daemon());
        thread.start();
        System.out.println("主线程开始");
    }
}
主线程开始
线程开始
线程执行
线程结束

使用join()

public class Daemon implements Runnable{
    @Override
    public void run() {
        System.out.println("线程开始");
        try {
            Thread.sleep(3000);
            System.out.println("线程执行");
        } catch (InterruptedException e) {
            System.out.println("离开blocked状态");
        }
        System.out.println("线程结束");
    }
    public static void main(String[] args) throws InterruptedException{
        Thread thread=new Thread(new Daemon());
        thread.start();
        thread.join();
        System.out.println("主线程开始");
    }
}
线程开始
线程执行
线程结束
主线程开始

程序启动后就开始运行主线程(因为新建线程使用了sleep(),让新建线程进入blocked状态,主线程可以占用cpu),对新建的线程使用join(),将其加入到主线程后,原本应该一来就运行的主线程,现在需要等到后面新建的线程执行完后,才能继续执行。
如果加入的线程处理时间太久,可以在join()时指定时间,如join(10000),表示加入的线程最多只能处理10秒。

停止线程

线程完成run()方法后,就进入dead状态。这时不能再次调用start()方法。
Thread类上的stop()方法是过时方法。
如果要停止线程,最好自行操作,让线程跑完应有的流程,而不是调用stop()方法

...
private boolean isContinue=true;
public void stop(){
    this.isContinue=false;
}
public void run(){
    while(isContinue){
        ...
    }
}
...

synchronized

public synchronized void add(Object o){...}

每个对象都有一个内部锁定。被标示为synchronized的区块会被监控,任何线程要执行synchronized区块都必须先获得指定的对象锁定。
如果线程A已获得对象锁定开始执行sychronized区块,线程B也想执行synchronized区块,线程B会因为无法获得对象锁定而进入等待对象锁定状态,直到线程A释放锁定(如执行完synchronized区块)。
在方法上标示sychronized,则执行方法必须获得该实例的指定。
实际上等待对象锁定时,也会进入线程的blocked状态

synchronized不仅可以声明在方法上,也可以描述句方式使用

public void add(Object o){
    synchronized(this){
        ...
    }
}
List<String> list=new ArrayList<String>();
synchronized(list){
    ...
    list.add("TheViper");//让ArrayList线程安全
}

这种方式不用锁定整个方法,只锁定会发生竞争状况的区块。获得锁定的线程执行完这个区块后,会立即释放锁定,其他线程就有机会再竞争这个锁定.
相比于将整个方法声明为synchronized,这种方式更有效率。
可以提供不同的对象作为锁定的来源

private Object lock1=new Object();
private Object lock2=new Object();
public void method1(){
    synchronized(lock1){
        data1++;
        ...
    }
}
public void method2(){
    synchronized(lock2){
        data2++;
        ...
    }
}

在两个synchronized区块里,没有公共的数据,方法。当有一个线程执行method1()而另一个线程执行method2()时,并不会引起共享存取问题,而且一个线程不会因为另一个线程获得锁定而阻塞.
java的synchronized提供的是可重入同步,也就是线程获得某对象锁定后,若执行过程中又要执行synchronized,而这个锁定对象的来源又和前面的是同一个,则可以直接执行。

死锁

public class Resource {
    synchronized void doSome(){
        
    }
    public synchronized void deal(Resource res){
        res.doSome();
    }
}
public class TheadDemo extends Thread{
    Resource res1;
    Resource res2;
    TheadDemo(Resource res1,Resource res2){
        this.res1=res1;
        this.res2=res2;
    }
    @Override
    public void run(){
        for(int i=0;i<10;i++)
            this.res1.deal(this.res2);
    }
    public static void main(String[] args){
        Resource res1=new Resource();
        Resource res2=new Resource();
        Thread thread1=new TheadDemo(res1,res2);
        Thread thread2=new TheadDemo(res2,res1);
        thread1.start();
        thread2.start();
    }
}

这段代码可能出现死锁,也可能不会。
如果出现的话,原因在于

  1. Thread thread1=new TheadDemo(res1,res2)=>this.res1.deal(this.res2)时,thread1获得res1的锁定

  2. Thread thread2=new TheadDemo(res2,res1)=>this.res2.deal(this.res1),thread2获得res2的锁定

  3. thread1线程res2.doSome(),准备获得res2的锁定,但是res2的锁定被thread2线程占用,于是thread1线程进入blocked状态

  4. thread2线程res1.doSome(),准备获得res1的锁定,但是res1的锁定被thread1线程占用,于是thread2线程进入blocked状态

volatile

synchronized所标志区块的特点:

  • 互斥性:synchronized区块在一个时间点上只能有一个线程执行它

  • 可见性:线程离开synchronized区块后,另一个线程面对的是上一个线程改变后的对象状态

在java中对于可见性的要求,可以使用volatile达到变量范围。
没用volatile,synchronized

public class Variable {
    static int i=0,j=0;
    static void deal(){
        i++;
        j++;
    }
    static void print(){
        System.out.println("i="+i+" j="+j);
    }
}
public class VariableTest1 extends Thread{
    @Override
    public void run(){
        while(true)
            Variable.print();
    }
}
public class VariableTest extends Thread{
    @Override
    public void run(){
        while(true)
            Variable.deal();
    }
    public static void main(String[] args){
        Thread t1=new VariableTest();
        Thread t2=new VariableTest1();
        t1.start();
        t2.start();
    }
}
...
i=638864948 j=638864993
i=638866963 j=638867006
i=638868897 j=638868941
i=638870928 j=638870974
...

可以看到j大于i,甚至可以远大于i.原因在于为了效率,线程可以快取变量的值。
t2调用Variable.print()从共享内存中取到变量i的值,并存储在自己的内存空间,如果此时cpu切换线程至t1,并不断的执行Variable.deal()多次,再切回t2,取得变量j的值,然后和i值一起输出。

如果在deal()和print()方法上标志synchronized,则t1每次调用deal()方法时,t2都必须等到t1释放锁定才能调用print()方法.t2每次调用print()方法也一样。
这种情况下,输出的i一定等于j.

在变量上声明volatile,表示变量是不稳定的,易变的。这样变量就可以在多线程情况下存取,保证了变量的可见性.换句话说,如果有线程修改了变量的值,另一个线程一定可以看到被修改的变量。
被标示为volatile的变量,不允许线程快取,变量的存取一定是在共享内存中进行。
下面将上面例子中的i,j声明为volatile.

volatile static int i=0,j=0;
...
i=34456445 j=34456448
i=34456620 j=34456623
i=34456789 j=34456791
i=34456959 j=34456961
...

可以看到没有出现j远大于i的情况,都是i略小于j.
事实上,ij的关系可以是大于,等于,小于三种之中的任一种。

  • i大于j

    i已经改变,但是j仍然是上一次操作的j.

  • i小于j

    第一次输出时,保存i,然后j++,第二次输出的是没有修改过的i和修改过的j.

  • i等于j

    输出的都是共享内存中修改过的值.

由此可见,volatile保证的是单一变量可见性,线程对变量的存取一定是在共享内存中进行,不会在自己的内存空间中快取变量。线程对共享内存中变量的存取,另一个线程一定看得到。

等待与通知

wait(),notify(),notifyAll()是Object类中的方法,可以通过这三个方法控制线程释放对象的锁定,或者通知线程参与锁定的竞争.

线程进入synchronized区块前,要先获得指定对象的锁定。而在执行synchronized区块代码时,若调用锁定对象的wait()方法,线程会释放对象锁定,并进入对象等待集合,其他线程这时可以竞争对象锁定,取得锁定的线程可以执行synchronized区块代码。
处于等待集合中的线程不会参与竞争cpu.wait()方法可以指定等待时间,时间到之后,线程会参与竞争cpu.如果指定时间为0或没指定,则线程会一直等待,直到被中断(interrupt)或通知(notify)可以参与竞争cpu.
锁定的对象调用notify()方法,会对象等待集合中随机通知一个线程参与竞争cpu.
如果调用的是notifyAll()方法,则等待集合中的所有线程都会参与竞争cpu.