天天看点

Java多线程笔记(二)

文章目录

  • ​​1. 并发,并行概念​​
  • ​​2. 进程,线程概念​​
  • ​​3. 类变量, 成员变量, 局部变量​​
  • ​​4. 线程安全​​
  • ​​5. 并发编程中,最重要的三个特性​​
  • ​​6. 线程状态的转换​​
  • ​​7. Java同步块​​
  • ​​8. 线程通信​​
  • ​​9. Thread.join()​​
  • ​​[参考文献]​​

1. 并发,并行概念

  • 并发(concurrent) : 一个时间段有多个程序都处于已启动运行的运行完毕之间,且这几个程序都是在同一个处理机上运行.
  • 并行(parallel) : 当一个系统有一个以上的CPU时,当一个CPU执行一个进程时, 另一个CPU可以执行另一个进程, 两个进程互不抢占CPU资源, 可以同时进行.
通俗理解, 排队取咖啡, 并发就是两个队伍一个咖啡机,交替取; 并行就是两个队伍两个咖啡机, 同时取.(Joe Armstrong图中意思)

2. 进程,线程概念

  • 进程 : 一个独立的程序就是一个进程
  • 线程 : 对于音乐播放器, 其中播放声音, 进度条, 歌词的滚动… 就是多个线程
其中多个进程直接切换需要进行​

​上下文切换​

​需要消耗资源, 因此考虑一个进程中同时运行多个"子任务"(线程)

​​CPU上下文切换详解​​

​​一文让你明白CPU上下文切换​​

对于Java语言, Java语言程序是运行在JVM上面的, 每一个JVM就是一个进程. 其中JVM是一个完整的计算机的一个模型.

3. 类变量, 成员变量, 局部变量

  • 共享变量 : 类变量 , 成员变量
  • 非共享变量 : 局部变量

4. 线程安全

多个线程同时访问共享变量的时候, 得到的结果和我们预期的一样, 就是线程安全.

允许被多个线程同时执行的代码称作线程安全的代码.

5. 并发编程中,最重要的三个特性

  • 原子性

    一个操作是不可中断的, 要么全部执行成功要么全部执行失败. 即使在多个线程一起执行的时候, 一段代码或者一个变量的操作, 再没有执行完之前, 就不会被其他线程所干扰.

    ​​

    ​synchronized满足原子性, 而volatile并不能保证原子性.​

  • 有序性

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

    如果不能满足有序性, 则可能得到的结果和预期的不同, 如编译器指令的重排序. 而​

    ​volatile包含禁止指令重排序的语义, 其具有有序性.​

    ​synchronized语义表示锁在同一时刻只能由一个线程进行获取, 当锁被占用后, 其他线程只能等待. 因此, synchronized就要求线程访问读写共享变量时只能"串行"执行, 因此​

    ​synchronized具有有序性​

    ​这篇博客解释了有序性在多线程中出现的重排序将导致程序出错 : 并发编程——原子性,可见性和有序性(有序性的介绍)

    对于指令重排序设计到JMM(Java Memory Model), 请看这篇博客 : ​​Java内存模型(JMM)总结​​

  • 可见性

    多个线程共享同一个变量, 当变量被其中一个线程修改后, 其他线程均可感知到, 该变量已被修改.

    ​​

    ​synchronized和volatile具有可见性​

总结

synchronized : 具有原子性, 有序性和可见性

volatile : 具有有序性和可见性

参考博客 :

​​三大性质总结:原子性,有序性,可见性​​

6. 线程状态的转换

Java多线程笔记(二)

这张图详细介绍了线程转换的六种状态 :

  1. 初始(NEW) : 新创建一个线程对象, 但还没调用start()方法
  2. 运行(RUNNABLE) : 就绪(READY) 与 运行中(RUNNING) 两种状态成为"运行"

    2.1 就绪(READY) : 线程创建后, 其他线程(比如main线程)调用了该对象的start()方法. 该状态的线程位于可运行线程池中, 等待被线程调度选中并分配CPU使用权

    2.2 运行中(RUNNING) : 就绪状态的线程获得了CPU时间片, 开始执行程序代码

  3. 阻塞(BLOCKED) : 表示线程阻塞于锁
  4. 等待(WAITING) : 进入该状态的线程需要等待其他线程做出一些特定的动作(通知或中断)
  5. 超时等待(TIMED_WAITING) : 该状态不同于WAITING, 它可以在指定的时间后自行返回
  6. 终止(TERMINATED) : 表示该线程以及执行完毕

7. Java同步块

Java同步块用来标记方法或者代码块是同步的.

通过​

​synchronized​

​关键字. 同步块在Java中是同步在某个对象上. 所有同步在一个对象上的同步块同事只能被一个线程进入并执行操作, 所有其他等待进入该同步块的线程将被阻塞, 直到执行该同步块的线程退出.

实例方法同步 : add方法

实例方法中的同步块 : delete方法

public class Test {
    int count = 0;
    
    public synchronized void add(int value) {
        this.count += value;
    }
    
    public void delte(int value) {
        synchronized(this) {
            this.count -= value;
        }
    }
    
}      

在上例中, 若他们所调用的是同一个实例对象, 同步的执行效果是等效的, 每次只有一个线程能够在两个同步块中任意一个方法内执行.

如果第两个同步块不是同步在​

​this​

​对象上, 而是其他对象上, 那么两个方法可以被线程同时执行.

静态方法同步 : add方法

静态方法中的同步块 : delete方法

public class Test {
    int count = 0;
    
    public static synchronized void add(int value) {
        this.count += value;
    }
    
    public void delete(int value) {
        synchronized(Test.class) {
            this.count -= value;
        }
    }
}      

这两个方法不允许同时被线程访问.

如果第二个同步块不是同步在​

​Test.class​

​这个对象上. 那么这两个方法可以同时被线程访问.

推荐阅读

​​​[译]Java虚拟机是如何执行线程同步的​​

8. 线程通信

线程通信的目标是使线程间能够互相发送信号. 另一方面, 线程通信使线程能够等待其他线程的信号.
  • 通过共享对象通信

    两个或多个线程必须获得指向一个共享实例的引用, 以便进行通信.

    在wait()/notify()机制中, 不要使用全局对象, 字符串常量等. 应该使用对应唯一的对象

  • wait(), notify()和notifyAll()

    这三个方法必须在同步代码块中调用. 如果一个线程没有持有对象锁, 调用这几个方法将抛出​

    ​java.lang.IllegalMonitorStateException​

    ​wait() : 该线程将变为非运行状态, 并且会释放所持有的监视器对象上的锁.

    notify() : 正在等待该对象的所有线程中将有一个线程被唤醒并允许执行(被唤醒的线程是随机的, 不可以指定唤醒哪个线程). 唤醒后的wait()方法必须等待notify()的线程退出了它自己的同步块才可以获得监视器对象的锁才可以继续执行(因为wait方法调用运行在同步块里面)

    notifyAll() : 在同一时刻将只有一个线程可以退出wait()方法, 因为每个线程在退出wait()钱必须获得监视器对象的锁

图片来源 : ​​Java线程的6种状态及切换(透彻讲解)​​
Java多线程笔记(二)

关于线程间通信, 有一个实例: 两个方法add和size; 启动两个线程, t1添加10个元素; t2对t1进行监控, 当size大小为5时, t2线程提示并退出, t1线程继续执行.

第一种方法 : wait/notify进行线程间通信

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class MyContainer {
    // volatile关键字 : list进行修改时, 将会通知其他线程
    volatile List list = new ArrayList();
    
    public void add(Object obj) {
        list.add(obj);
    }
    
    public int size() {
        return list.size();
    }
    
    public static void main(String[] args) {
        MyContainer mc = new MyContainer();
        
        final Object lock = new Object();
        
        new Thread(() -> {
            // 首先启动t2线程若size不为5, 则等待t1线程
            System.out.println("t2启动");
            synchronized(lock) {
                if (mc.size() != 5) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t2结束");
                // 因为只有两个线程, 所以无需使用notifyAll()唤醒所有线程
                lock.notify();
            }
        }, "t2").start();
        
        new Thread(() -> {
            System.out.println("t1启动");
            synchronized {
                     for (int i = 0; i < 10; i++) {
                        mc.add(new Object());
                        System.out.println("add" + i);

                        if (mc.size() == 5) {
                            lock.notify();
                            try {
                                // 需要释放锁来让t2线程执行, 否则t2将会等t1执行结束后, 才会执行t2
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }

                        try {
                        TimeUnit.SECONDS.sleep(1);                    
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
            }
        }, "t1").start();
    }
    
}      

线程间通信太繁琐, 可以使用jdk已经提供好的Latch来替代wait/notify.

方法二 : 使用CountDownLatch :

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class MyContainer3 {
    // 添加volatile, 使t2能够得到通知
    volatile List list = new ArrayList();

    public void add(Object object) {
        list.add(object);
    }

    public int size() {
        return list.size();
    }

    public static void main(String[] args) {
        MyContainer3 mc = new MyContainer3();

        CountDownLatch latch = new CountDownLatch(1);

        new Thread(() -> {
            System.out.println("t2启动");
            if (mc.size() != 5) {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t2结束");
        }, "t2").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            System.out.println("t1启动");
            for (int i = 0; i < 10; i++) {
                mc.add(new Object());
                System.out.println("add" + i);

                if (mc.size() == 5) {
                    latch.countDown();
                }

                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1结束");
        }, "t1").start();
    }
}      

9. Thread.join()

该方法把指定的线程加入到当前线程, 可以将两个交替执行的线程合并为顺序执行的线程.

比如主线程调用正在执行的线程A的join()方法, 那么主线程将会等待线程A执行完毕后, 才会继续执行.

Thread.join()的源码调用join(0)即下面代码 :

/**
     * Waits at most {@code millis} milliseconds for this thread to
     * die. A timeout of {@code 0} means to wait forever.
     *
     * <p> This implementation uses a loop of {@code this.wait} calls
     * conditioned on {@code this.isAlive}. As a thread terminates the
     * {@code this.notifyAll} method is invoked. It is recommended that
     * applications not use {@code wait}, {@code notify}, or
     * {@code notifyAll} on {@code Thread} instances.
     *
     * @param  millis
     *         the time to wait in milliseconds
     */
    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;
            }
        }
    }      
public class JoinTest implements Runnable {
    public 
}      

[参考文献]

  1. ​​Java并发性和多线程 v1.2​​
  2. ​​Hollis博客​​