Java核心技术之多线程

生命周期

线程生命周期:new(新建)、runnable(就绪)、running(运行)、blocked(阻塞)、dead(死亡)

并发编程中重要的三个问题

1. 原子性

即:一个操作或多个操作要么不执行,要么全部执行且执行过程中不会被打断
最典型的一个例子就是银行的转账问题:A账户向B账户转账100元,那么必须保证两个操作:A账户减去100元,B账户增加100元。这两个操作缺一不可

2. 可见性

指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到这个被修改的值
举个栗子:

//线程1执行代码
int i = 0;
i =1;

//线程2执行代码
int j = i;

假设这两个线程是在两个不同的CPU中执行,那么在线程1执行i = 1时,会将i的初始值加载到线程1所在地CPU1的高速缓存中,然后进行赋值操作,此时i的值在CPU1的高速缓存中值为1,但是还没有写入主存中,此时主存的值还是0。这时线程2执行对j的赋值操作,会先读取i的值,此时读到的数还是0。也因此会使得j的值是0。
这就是可见性问题,线程1修改了i的值,但线程2却没有立即看到线程1修改后的i值。

3. 有序性

即程序的执行顺序按照代码的先后顺序执行。

int i = 0;
int j = 1;
i = 2;  //语句1
j = 3;  //语句2

在这段代码中,语句1一定比语句2先执行嘛?从代码顺序上看,确实是这样。但是,JVM在将代码编译成最终的执行代码时,为了提高执行效率,可能会对代码的运行顺序进行调整,它不保证代码运行顺序一致,但是保证最终的运行结果一致。
比如上面的代码,语句1与语句2执行的先后顺序并不影响最终的执行结果,因此在执行过程中,语句2可能优于语句1先执行。
在代码的重排序过程中会考虑数据的依赖性问题,如果一个指令2必须用到另一个指令1的结果,那么处理器运行时一定会保证指令1在指令2之前执行。
代码的重排序不会影响到单线程的执行结果,但是对于多线程就不一定了。

//线程1
int a = 0;  //--------------语句1
boolean b = false;  //------语句2
a = 10;  //-----------------语句3
b = true;  //---------------语句4

//线程2
int j = 5;
while(!b){
    // do something
}
j = a;  //------------------语句5

在线程1中,语句1、3之间或语句2、4有数据依赖,不会被重新排序,而语句3、4之间是有可能重新排序的,假如他们之间发生了重排序,线程1先执行了语句4,此时时间片轮转,线程2执行,会跳出循环,执行赋值,而此时a的值还等于0,导致j的值为0。程序就可能出错。

创建线程

  1. Java应用程序的main方法是一个线程,在JVM启动的时候进行调用,线程名字为:main
  2. 实现一个线程,必须创建一个Thread实例,并override run方法,并调用start方法
  3. 在JVM启动等等时候,实际上有多个线程,但至少有一个是守护线程
  4. 当你调用一个线程start方法时,至少有两个线程,一个是被调用的线程,另一个就是执行调用的线程
  5. 创建的Thread对象,默认的线程名,以Thread-开头,后跟从0开始的数字

    例如:
    Thread-0
    Thread-1
    Thread-2

  6. 如果在构造Thread的时候没有传递Runable接口的实例,或者没有复写Thread的run方法,则该Thread将不会调用任何内容,如果传递了Runable接口实例,或复写了run方法,则执行该方法的逻辑代码
  7. 如果构造线程对象时未传入ThreadGroup,则会默认将父线程的ThreadGroup作为该线程的ThreadGroup。main线程的ThreadGroup为main
  8. 关于JVM内存可以看这篇文章:JVM之Java内存结构

Thread的构造函数

Thread中提供了8个构造函数,包括

Thread()
//Allocates a new Thread object.

Thread(Runnable target)
//Allocates a new Thread object.

Thread(Runnable target, String name)
//Allocates a new Thread object.

Thread(String name)
//Allocates a new Thread object.

Thread(ThreadGroup group, Runnable target)
//Allocates a new Thread object.

Thread(ThreadGroup group, Runnable target, String name)
//Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group.

Thread(ThreadGroup group, Runnable target, String name, long stackSize)
//Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group, and has the specified stack size.

Thread(ThreadGroup group, String name)
//Allocates a new Thread object.

Thread()

在Java官方的API文档中,如此介绍该方法:

Allocates a new Thread object. This constructor has the same effect as Thread (null, null, gname), where gname is a newly generated name. Automatically generated names are of the form “Thread-“+n, where n is an integer.
分配一个新的线程对象,这个构造函数相当于Thread(null,null,gname)。gname是一个自动生成的名字,格式是”Thread”+n,n是整数

查看其对应的源码:

/* For autonumbering anonymous threads. */
/* 用于对匿名线程自动编号 */
private static int threadInitNumber;

private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
    init(g, target, name, stackSize, null, true);
}

就是说:每当创建一个匿名的线程对象时,编号就会加一。而在Java中,int类型的成员变量初始化为0,所以会从Thread-0开始起名字

Thread(ThreadGroup group, Runnable target, String name)

这个函数需要传入一个ThreadGroup对象,而在上面的无参构造方法中,传入了null,查看源码之后,发现如果传入null则使用父线程的ThreadGroup,例如对main方法创建的线程,其父线程就是main,对应的ThreadGroup的名字也是main。

private void init(ThreadGroup g, Runnable target, String name,
                    long stackSize, AccessControlContext acc,
                    boolean inheritThreadLocals) {
    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
        /* If the security doesn't have a strong opinion of the matter
            use the parent thread group. */
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }
    /* 此处省略许多代码 */
}

第二个参数是一个Runable接口实例对象,Runnable接口必须实现run方法,这样通过start方法就可以创建一个你想要的线程了。

public void run() {
    if (target != null) {
        target.run();
    }
}

Thread(ThreadGroup group, Runnable target, String name, long stackSize)

这个构造方法比上个多了一个stackSize参数,这个参数涉及到JVM的内存结构。关于JVM的内存结构可以查看这个文章:JVM之Java内存结构
这个方面我还不是很清楚

Daemon线程(守护线程)

Java中分为用户进程和守护进程,两者并没有什么本质的区别,唯一不同的是,如果用户线程全部运行结束退出了,那么守护线程无论运没运行完都会推出。毕竟被守护的都没有了,还守护什么呢。使用守护线程的时候有以下几点需要注意:

  1. thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
  2. 在Daemon线程中产生的新线程也是Daemon的。
  3. 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
  4. 创建守护线程的父线程如果结束,守护线程也会结束

守护线程使用的例子:
在网络连接中,有一种端与端之间保持连接的方式,叫长连接,用以保持两端之间的通信一直持续,确认对方存在的方式是每隔一段时间发送一个心跳包,确认对方还存在。这时发送这个心跳包就需要设置成守护线程,这样,如果链接不存在了就会直接停止发送心跳包,不需要手动关闭这个线程。:

public static void main(String[] args) {

    System.out.println("link start");

    Thread t = new Thread(()-> {
        Thread check = new Thread(() -> {
            try {
                while (true){
                    System.out.println("Do something for health check.");
                    Thread.sleep(1_000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        check.setDaemon(true);
        check.start();

        try {
            Thread.sleep(10_000);
            System.out.println("link thread finish done.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    t.start();
}

线程优先级

Java中将线程分为10个数字,最高优先级(10)、默认优先级(5)、最低优先级(1)。新建的线程默认优先级为5。较高的优先级线程在线程的调度策略中被优先调用的概率较高,但不保证最先执行。同样,较低的优先级进程被优先调用的概率较低,但不一定最后执行。

线程ID

通过Thread中的getId方法可以获取到线程的ID
JavaAPI文档中对ID这样描述:

The thread ID is a positive long number generated when this thread was created. The thread ID is unique and remains unchanged during its lifetime. When a thread is terminated, this thread ID may be reused.
这个线程ID是一个正增长的数字,在线程存活时候是唯一且不可更改的。当线程结束,这个ID可以再次使用

Thread的join方法

先看一段代码:

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() ->{
        //循环打印1-1000
        IntStream.range(1,1000)
                .forEach(i -> System.out.println(Thread.currentThread().getName() + " => " + i));
    }, "t1");

    t1.start();
    t1.join();

    IntStream.range(1,1000)
            .forEach(i -> System.out.println(Thread.currentThread().getName() + " => " + i));
}

上面的方法的执行结果中,如果没有t1.join();,main线程和t1线程会交替执行输出。但加上t1.join();之后,main线程会等待t1线程执行完毕在执行。

再看一段代码,我们来做个试验,这时main创建两个子线程,通过调整顺序,看看两个子线程的执行结果。

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() ->{
        //循环打印1-1000
        IntStream.range(1,1000)
                .forEach(i -> System.out.println(Thread.currentThread().getName() + " => " + i));
    }, "t1");
    Thread t2 = new Thread(() ->{
        //循环打印1-1000
        IntStream.range(1,1000)
                .forEach(i -> System.out.println(Thread.currentThread().getName() + " => " + i));
    }, "t2");

    t1.start();
    t1.join();

    t2.start();
    t2.join();

    //第二次:
    /*
    t1.start();
    t2.start();

    t1.join();
    t2.join();
    */


    IntStream.range(1,1000)
            .forEach(i -> System.out.println(Thread.currentThread().getName() + " => " + i));
}

第一次执行结果中当t1线程执行完之后,才输出t2的执行结果,说明main线程等待t1线程执行完毕才创建了t2线程,第二次两个线程交替执行,说明join方法只对父线程有效,两个兄弟线程之间互不影响。

join(long millis)和join(long millis, int nanos)方法

这两个方法分别传入了毫秒和毫秒+纳秒的时间,设定了最大的等待时间。如果超过这个时间线程还没有执行完,则继续执行。

进程中断机制(interrupt)

Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程也不一定要立即停止正在做的事。中断是礼貌地请求另一个线程在它愿意并且方便的时候停止它正在做的事情。有些方法例如 Thread.sleep(),很认真地对待这样的请求,但每个方法不是一定要对中断作出响应。
中断的协作特性所带来的一个好处是,它为安全地构造可取消活动提供更大的灵活性。我们很少希望一个活动立即停止;如果活动在正在进行更新的时候被取消,那么程序数据结构可能处于不一致状态。中断允许一个可取消活动来清理正在进行的工作,恢复不变量,通知其他活动它要被取消,然后才终止。

Java中提供了几个操作这个中断的方法:

public static boolean interrupted()
// 返回线程中断状态,并将中断状态重置(即:如果线程当前是中断状态,调用此方法之后返回true,并将中断状态设为false;而如果不是中断状态,则不会改变)

public void interrupt()
// 将这个线程对象设为中断状态

public boolean isInterrupted()
// 返回线程对象的中断状态

如果一个线程在阻塞状态中,例如线程调用了Object.wait()、Thread.sleep()、Thread.join()以及可中断的通道上的 I/O 操作方法时,这时如果线程检测到中断标识为true,就会抛出InterruptedException异常,并将中断状态重置。

如何优雅的结束线程

实现可取消的任务

阻塞方法可能因为等不到所等的事件而无法终止,因此令阻塞方法可取消就非常有用(如果长时间运行的非阻塞方法是可取消的,那么通常也非常有用)。可取消操作是指能从外部使之在正常完成之前终止的操作。由 Thread 提供并受 Thread.sleep() 和 Object.wait() 支持的中断机制就是一种取消机制;它允许一个线程请求另一个线程停止它正在做的事情。当一个方法抛出 InterruptedException 时,它是在告诉您,如果执行该方法的线程被中断,它将尝试停止它正在做的事情而提前返回,并通过抛出 InterruptedException 表明它提前返回。 行为良好的阻塞库方法应该能对中断作出响应并抛出 InterruptedException,以便能够用于可取消活动中,而不至于影响响应。

处理InterruptedException

一般较为友好的处理方式是自己不管处不处理都要抛出这个异常,以便于它的调用者能够知道中断,并作出响应。当不能抛出这个异常的时候,例如Runnable的方法调用一个可以中断的方法时,这时应当调用interrupt方法保留中断的状态,以便于高层处理者能够接收到中断信号。

较为粗暴的结束方式

在一些时候,程序会运行一些比较耗时间,但是又不会处理中断请求的方法,比如网络连接,在网络不好的时候可能会等待很长时间,如何去取消呢,一种方法是将它设置为守护线程,由另一个线程去调用它,并且由这个线程处理中断。当线程接收到中断信号时就退出,守护线程也会随之退出。

数据同步与锁

public class SynchronizedTest implements Runnable{
    private static int index = 0;
    private static final int MAX = 10;

    public static void test1(){
        while (index < MAX){
            try {
                Thread.sleep(30);  //语句1
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程" + Thread.currentThread().getName() + "获取"+ index++);
        }
    }

    @Override
    public void run() {
        test1();
    }

    public static void main(String[] args) {
        Thread a = new Thread(new SynchronizedTest(), "a");
        Thread b = new Thread(new SynchronizedTest(), "b");
        Thread c = new Thread(new SynchronizedTest(), "c");

        a.start();
        b.start();
        c.start();
    }
}

上面的执行结果:

线程a获取0
线程c获取1
线程b获取2
线程a获取3
线程c获取4
线程b获取5
线程a获取6
线程b获取7
线程c获取8
线程a获取9
线程b获取10
线程c获取11

可以发现,最终的结果会变到11,这是因为当b线程获取到index时是9,然后进入到了阻塞状态,此时a、c也获取到了9,因此他们都会继续向下执行,不会退出循环,因此三个线程都会使index自增,最终导致结果与预期不一致。
我们可以通过增加一个锁来解决这个问题。

public class SynchronizedTest implements Runnable{
    private static int index = 0;
    private static final int MAX = 10;
    private static final Object MONITOR = new Object();

    public static void test1(){
        synchronized (MONITOR) {
            while (index < MAX){
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程" + Thread.currentThread().getName() + "获取"+ (index++));
            }
        }
    }
}

我们先获取MONITOR对象的锁,这样就保证了同一时间内只有一个线程对index进行操作。

同步代码块

public class SynchronizedRunnable implements Runnable {
    private static int index = 0;
    private static final int MAX = 50;
    @Override
    public synchronized void run() {
        while (index < MAX){
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程" + Thread.currentThread().getName() + "获取"+ (index++));
        }
    }

    public static void main(String[] args) {
        Runnable runnable = new SynchronizedRunnable();

        Thread a = new Thread(runnable, "a");
        Thread b = new Thread(runnable, "b");
        Thread c = new Thread(runnable, "c");

        a.start();
        b.start();
        c.start();
    }
}
线程a获取0
线程a获取1
线程a获取2
线程a获取3
线程a获取4
线程a获取5
线程a获取6
线程a获取7
线程a获取8
线程a获取9
线程a获取10
线程a获取11
线程a获取12
线程a获取13
线程a获取14
线程a获取15
线程a获取16
线程a获取17
线程a获取18
线程a获取19

在方法块中,锁的对象是this。

关于this锁

public class SynchronizedThis {
    public static void main(String[] args) {
        ThisLock thisLock = new ThisLock();
        Thread t1 = new Thread(() -> {
            thisLock.m1();
        }, "T1");
        Thread t2 = new Thread(() -> {
            thisLock.m2();
        }, "T2");
        t1.start();
        t2.start();
    }
}

class ThisLock {
    public synchronized void m1 (){
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void m2 (){
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在上面的实验中,m1加锁,m2不加。最后两个线程会几乎同时输出。而如果增加m2的锁,那么两个线程之间会出现互斥现象。
上面的m1方法相当于下面的代码:

public void m1 (){
    synchronized (this) {
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

关于class锁(静态锁)

public class SynchronizedStatic {

    public static void main(String[] args) {
        new Thread(() -> {
            SynchronizedStatic.m1();
        }, "T1").start();
        new Thread(() -> {
            SynchronizedStatic.m2();
        }, "T2").start();
        new Thread(() -> {
            SynchronizedStatic.m3();
        }, "T3").start();
    }

    public synchronized static void m1 (){
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized static void m2 (){
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void m3 (){
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个实验中,m1、m2、m3方法都是静态方法,对m1、m2加锁,m3不加。运行之后发现,T1与T3几乎同时输出,而T2则等待了10秒。这说明class锁只影响静态方法。

死锁实验

参考资料


 上一篇
/proc/cpuinfo 文件信息 /proc/cpuinfo 文件信息
Linux中我们可以通过cat /proc/cpuinfo来查看CPU的一些信息 [root@name ~]# cat /proc/cpuinfo processor : 0 //系统中逻辑处理核的编号。对于单核处理器,则课
2019-04-26
下一篇 
Maven项目报错:Classpath entry org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER will not be exported or published. Runtime ClassNotFoundExceptions may result. Maven项目报错:Classpath entry org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER will not be exported or published. Runtime ClassNotFoundExceptions may result.
eclipse中创建Maven项目之后出现警告:Classpath entry org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER will not be exported or published. Ru
2019-03-13
  目录