Skip to content

Latest commit

 

History

History
454 lines (360 loc) · 19.3 KB

File metadata and controls

454 lines (360 loc) · 19.3 KB

Java学习笔记-02 并发

整理参考:
1、Java核心技术卷1
2、Java多线程编程核心技术
3、CDSN:Java多线程学习
4、CSDN:jvm主内存和工作内存

多任务:同一刻运行多个程序的能力。
多线程:一个程序同时执行多个任务。通常每一个任务称为一个线程(thread),是线程控制的简称。
区别:每个进程拥有自己的一整套变量,而线程之间共享数据。
为什么多线程:线程是轻量级进程,是程序执行的最小单位。线程之间的切换和调度成本远小于进程。

0 目录

1 线程的实现

1.1 用Runnable接口实现

  将任务代码移到实现了Runnable接口的类的run方法中。Runnable接口非常简单,只有一个方法:

public interface Runnable{
    void run();
}

  Runnable是一个函数式接口,可以用lambda表达式建立一个实例:Runnable r=()->{task code};
  由Runnable创建一个Thread对象,启动线程:

Thread t=new Thread(r);
t.start();

1.2 用Thread子类实现

  也可以通过构建一个Thread类的子类定义一个线程,然后构造一个子类的对象,并调用start方法。不过这种方法不再推荐,应该将要并行运行的任务和运行机制解耦合。

class MyThread extends Thread{
    public void run(){
        task code;
    }
}

注意:直接调用Thread类或Runnable对象的run方法,只会执行同一个线程中的任务,而不会启动新线程。而调用Thread.start方法,会创建一个执行run方法的新线程。

1.3 线程池

  待整理。

1.4 常用方法

方法名 作用
currentThread() 返回当前正在执行的线程对象的引用
getId() 返回此线程的标识符
getName() 返回此线程的名称
getPriority() 返回此线程的优先级
setPriority() 更改线程的优先级
isAlive() 测试此线程是否活动状态(线程处于正在运行或准备运行的状态)
sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂时停止
interrupt() 中断这个线程
interrupted() 测试线程是否处于中断状态,并将状态标志改为false
isInterrupted() 测试线程是否处于中断状态,但不修改状态标志
setName(String name) 将线程改名
isDaemon() 测试线程是否为守护线程
setDaemon(boolean on) 将线程标记为守护线程
join() 等待线程终止,如子线程调用该方法,主线程需等子线程执行结束才能执行
yield() 放弃当前CPU资源(但是放弃时间不确定,可能会重新获得CPU时间)

1.5 中断线程

  当线程的run方法执行完方法体中最后一句语句,并经由执行return语句返回时,或者出现了方法中没有捕获的异常时,线程将终止。
  没有可以强制线程终止的方法。但是,interrupt方法可以请求终止线程。
  当对一个线程调用interrupt方法时,线程的中断状态将被置位。每个线程应该不时地检查这个标志。

while(!Thread.currentThread().isInterrupted() && more work to do){
    do more work
}

  如果线程被阻塞,就无法检测终端状态,当一个被阻塞的线程(调用sleep或wait)调用interrupt方法时,阻塞调用将会被InterruptedException异常中断。

2 线程属性

2.1 线程的状态

image

  • 新创建:如new Thread(r),该线程还没有开始运行。
  • 可运行:一旦调用start方法,线程处于runnable状态。一个可运行的线程可能正在运行也可能没有运行。
  • 被阻塞和等待:
    • 线程试图获取内部对象锁,而锁被其他线程占用,线程进入 阻塞 状态
    • 当线程等待另一个线程通知调度器一个条件时,进入 等待 状态
    • 有几个方法有一个超时参数,调用它们导致线程进入 计时等待 状态
  • 被终止:run方法正常退出或没有捕获的异常终止了run方法。

2.2 线程优先级

  每一个线程有一个优先级,默认继承它的父线程的优先级。可以用setPriority()方法提高或降低任何一个线程的优先级(最小为1,最大为10,普通线程的优先级默认是5)。

2.3 守护线程

  通过调用t.setDaemon(true)将线程转为守护线程,守护线程唯一用途是为其他线程提供服务。当所有非守护线程结束之后,守护线程会被全部杀死。
  最常见的守护线程:垃圾回收线程。

3 并发访问

  如果两个线程存取相同的对象,并且每一个线程都调用了一个修改对象状态的方法,会发生竞争条件(race condition)。
  有两种机制可以防止代码块受并发访问的干扰。Java语言提供了一个synchronized关键字达到这一目的,synchronized关键字自动提供了一个锁以及相关的“条件”,并且JavaSE5.0引入了ReentrantLock类。

3.1 synchronized关键字

  synchronized取得的锁是对象锁,如果多个线程访问的是同一个对象,哪个线程先执行带有synchronized关键字的方法,则哪个线程就持有该方法,那么其他线程只能呈现等待状态。如果多个线程访问的是多个对象,多个对象会产生多个锁。

3.1.1 锁重入

  锁是可重入的,因为线程可以重复地获得已经持有的锁。当一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入,就会造成死锁。

class Test{
    synchronized public void test1(){
        System.out.println("test1");
        test2();
    }

    synchronized public void test2(){
        System.out.println("test2");
        test3();
    }

    synchronized public void test3(){
        System.out.println("test3");
    }
}

public class TestSync {
    public static void main(String[] args) {
        Test t=new Test();
        Thread thread=new Thread(()->t.test1());
        thread.start();
    }
}

//运行结果
test1
test2
test3

  可重入锁也支持在父子类继承的环境中。当存在父子继承关系的时候,子类完全可以通过可重入锁调用父类的同步方法。
  但是同步不能继承,所以还是需要子类方法中添加synchronized关键字。

3.1.2 同步代码块

  使用synchronized关键字声明方法有些时候存在着一些弊端,比如一个线程A调用同步方法后获得锁,那么另一个方法就需要等待A执行完,但是如果A执行的是一个费时的任务,就会特别耗时。因此还有另外一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞:

public void method(){
    other section
    synchronized(obj){//this is the syntax for a synchronized block
        critical section
    }
}

  当一个线程访问一个对象的synchronized同步代码块的时候,另一个线程仍然可以访问该对象的非synchronized同步代码块。
  同步代码块的obj又称为对象监视器,当两个线程使用了同一个对象监视器的时候,运行结果是同步的;当使用不同的对象监视器的时候,运行结果就不是同步的了。

3.1.3 类锁和对象锁

  synchronized关键字如果加上static静态方法,或者使用synchronized(class)代码块,都是给Class类上锁,而synchronized关键字加到非静态方法方法上只是给对象加锁。
  类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不相干的,但是每个类只有一个类锁。如果synchronized同时修饰了静态方法和实例方法,那么它们将会交替执行,因为类锁和对象锁是不一样的锁。(下面这个程序和它的执行结果说明了这一点)

public class TestSync{
    public synchronized void test1(){
        int i = 5;
        while( i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try{
                Thread.sleep(500);
            }catch (InterruptedException e){}
        }
    }

    public static synchronized void test2(){
        int i = 5;
        while( i-- > 0){
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try{
                Thread.sleep(500);
            }catch (InterruptedException e){}
        }
    }

    public static void main(String[] args){
        final TestSync ts = new TestSync();
        Thread test1 = new Thread(()->ts.test1(),"test1");
        Thread test2 = new Thread(()->TestSync.test2(),"test2");
        test1.start();
        test2.start();
    }
}

//运行结果
test1 : 4
test2 : 4
test2 : 3
test1 : 3
test1 : 2
test2 : 2
test1 : 1
test2 : 1
test2 : 0
test1 : 0

  另外,线程获得对象锁的同时,也可以获得类锁,两者可以并存。

3.2 volatile关键字

  要理解volatile关键字,首先理解下面这个代码:

class RunThread extends Thread{
    private boolean isRunning=true;
    int m;
    public boolean isRunning(){
        return isRunning;
    }
    public void setRunning(boolean isRunning){
        System.out.println(Thread.currentThread().getName());
        this.isRunning=isRunning;
    }
    public void run(){
        System.out.println(Thread.currentThread().getName());
        while(isRunning==true)  m+=1;
        System.out.println(m);
        System.out.println("Thread ends");
    }
}

public class Run{
    public static void main(String[] args) throws InterruptedException{
        RunThread thread=new RunThread();
        thread.start();
        Thread.sleep(2000);
        thread.setRunning(false);
        System.out.println("isRunning="+thread.isRunning());
    }
}

//运行结果
Thread-0
main
isRunning=false

  可以看到,程序并没有输出“Thread ends”,也就是sleep结束之后,线程仍然在执行 while循环。这个问题的原因就在于Java的内存模型JMM,JDK1.2之后,JMM规定了所有的变量都存储在主内存中,而每个线程有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对于变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。
  所以就很好理解,虽然修改了isRunning的值,但是我们可以看到,在执行setRunning()方法的时候,虚拟机此时运行的线程是main线程,也就是说Thread-0线程的本地内存中的值其实并没有被改变,所以就导致了死循环。
  解决办法就是加上volatile关键字,因为volatile关键字的语义是成员变量对不同线程的可见性(另外它还有一层语义就是防止程序的重排序),用volatile修饰的变量每次被线程访问的时候,都强迫从主内存中重读该变量的值,当变量发生变化的时候,强迫线程将变化值写回主内存,就相当与保持了数据对不同线程的同步可见性。

补充:在while循环中加入输出语句或者sleep方法也可以停止死循环,因为JVM会保证内存的可见性,当CPU有时间它就会尽力去保证变量的更新,但如果CPU一直被占用,就不能保证内存的可见性了。

3.2.1 volatile和synchronized的比较

  • 性能:volatile关键字是轻量级的线程同步,不会发生阻塞;而synchronized可能引发阻塞,但是在JavaSE1.6之后,对锁进行了优化后,性能和volatile相差不大
  • 作用域:volatile只能修饰变量;synchronized可以修饰方法已经代码块
  • 同步安全:volatile并不能保证数据原子性,而synchronized可以保证多线程之间访问的同步性

3.2.2 volatile不能保证原子性

原子性:一个操作是不可中断的,要么全部执行成功,要么全部执行失败。

  因为volatile只能保证每次用数据的时候从主存开始读,然后再写回主存,但它并不能保证在读和写之间的空隙里,没有另外的线程访问该变量,所以volatile并不能保证变量的原子性,要保证原子性可以使用synchronized关键字。

public class TestVolatile {
    private static volatile long count = 0;
    public static void main(String[] args) {
        TestThread t1 = new TestThread();
        t1.start();
        TestThread t2 = new TestThread();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {}
        System.out.println("final count is: " + count);
    }
    private static class TestThread extends Thread {
        public void run() {
            long i = 0;
            while (i++ < 10000000L) count++;
        }
    }
}

//运行结果
第1次final count is: 7806434
第2次final count is: 6771763
第3次final count is: 8327083

3.2.3 原子类

  因为volatile并不能保证原子性,所以除了使用synchronized对方法进行同步之外,还可以使用原子类AtomicInteger来实现修饰变量的同步。原子类可以在没有锁的情况下做到线程安全。

  同样的,我们将上面一个证明volatile不具备原子性的例子中的变量改成原子类再执行,结果就是正确的,而且在线程方法中输出的count也是有序的。

import java.util.concurrent.atomic.*;
public class TestAtomic {
    private static AtomicLong count = new AtomicLong(0);
    public static void main(String[] args) {
        TestThread t1 = new TestThread();
        t1.start();
        TestThread t2 = new TestThread();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {}
        System.out.println("final count is: " + count);
    }
    private static class TestThread extends Thread {
        public void run() {
            long i = 0;
            while (i++ < 10000000L) count.incrementAndGet();
        }
    }
}

//运行结果
第1次final count is: 20000000
第2次final count is: 20000000
第3次final count is: 20000000

  但是,原子类也并不是绝对安全的。

import java.util.concurrent.atomic.AtomicInteger;
class TestThread extends Thread{
    private static AtomicInteger count=new AtomicInteger(0);
    public void run(){
        for (int i=0;i<100;i++) {
            System.out.println(Thread.currentThread().getName()+" "+count.incrementAndGet());
        }
    }
}
public class Run{
    public static void main(String[] args){
        TestThread[] t=new TestThread[100];
        for (int i=0;i<100;i++) t[i]=new TestThread();
        for (int i=0;i<100;i++) t[i].start();
    }
}

//运行结果(局部)
Thread-40 9974
Thread-40 9975
Thread-76 9959
Thread-76 9977
...
Thread-76 9989
Thread-40 9976
Thread-40 9991

  可以发现,虽然最终的结果是正确的,但是在输出过程中出现了乱序的情况,这是因为虽然原子类的操作是原子性的,但是方法之间的调用不是原子性的,用javap -c指令将java的字节码调出来看可以知道,sout方法在调用的时候进行了14步操作,所以很容易让另外一个线程占用CPU时间片,从而导致乱序。如果在方法中存在逻辑关系,那么就很有可能因为乱序而导致线程不安全。

8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
11: new           #3                  // class java/lang/StringBuilder
14: dup
15: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
18: invokestatic  #5                  // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
21: invokevirtual #6                  // Method java/lang/Thread.getName:()Ljava/lang/String;
24: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
27: ldc           #8                  // String
29: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
32: getstatic     #9                  // Field count:Ljava/util/concurrent/atomic/AtomicInteger;
35: invokevirtual #10                 // Method java/util/concurrent/atomic/AtomicInteger.incrementAndGet:()I
38: invokevirtual #11                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
41: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
44: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

4 线程间通信

4.1 等待/通知

  内部对象锁有一个相关条件,wait方法添加一个线程到等待集中,notify All/notify方法解除等待线程的等待状态。

方法 描述 调用
notifyAll() 解除那些在该对象上调用wait方法的线程的等待状态 同步方法或同步块中调用
notify() 随机选择一个在该对象上调用wait方法的线程,解除其等待状态 只能在一个同步方法或同步块中调用
wait() 导致线程进入等待状态直到它被通知 只能在一个同步方法中调用

  当执行方法wait()之后,锁将自动被释放,但是执行完notify()方法后,锁不会自动释放,必须在执行完notify()方法所在的synchronized代码块之后才释放。
  当线程处于等待状态的时候,对线程对象调用interrupt方法会出现InterruptException异常。

4.2 join方法

  在很多情况下,主线程会发起子线程,如果子线程需要进行耗时运算,那么主线程很有可能在子线程执行完成之前结束,但是往往主线程会需要子线程的运行结果,即主线程要等待子线程执行完成之后才能结束,这个时候就需要join()方法了。和wait方法类似,如果方法带有时间参数,那么线程会在时间到期的时候返回。
  另外,和sleep()方法不同,join方法和wait方法都会释放锁。

/**
*  Waits at most <code>millis</code> milliseconds for this thread to  
* die. A timeout of <code>0</code> means to wait forever.   
*/

public final synchronized void join(long millis)    throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;
 
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
        
    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

millis==0时,会进入while(isAlive())循环,只要子线程是活的,调用join()方法的线程就会进入wait状态。

4.3 CyclicBarrier

参考文档

4.4 CountDownLatch