JAVA多线程学习笔记
进程和线程
计算机基础知识入门
进程和线程
-
进程:一个用户程序(QQ,微信)
-
线程:进程中的一个实体,一个线程可有多个线程
-
进程是内存分配的基本单位,线程是计算资源分配的基本单位
-
可在任务管理器查看单当前计算机的进程和线程数
-
上下文:线程执行的环境与数据,在线程切换运行时需要保存上下文数据,用户线程运行时成为用户态,系统内核调度时称为内核态
创建线程的方法
-
定义类继承Thread重写run方法
package Demo1; public class Test1 { static class UserThread extends Thread{ @Override public void run() { System.out.println(2); } } public static void main(String[] args) throws InterruptedException { System.out.println(1); new UserThread().start(); Thread.sleep(1000); System.out.println(3); } }
-
实现Runnable接口
package Demo1; public class Test2 { static class Task implements Runnable{ @Override public void run() { System.out.println(2); } } public static void main(String[] args) throws InterruptedException { System.out.println(1); new Thread(new Task()).start(); Thread.sleep(1000); System.out.println(3); } }
-
使用Lammbda表达式本质也是重写runable接口,因为该接口只有一个重写方法,所以可用Lammbda表达式创建
package Demo1; public class Test3 { public static void main(String[] args) throws InterruptedException { System.out.println(1); new Thread(()->{ System.out.println(2); }).start(); Thread.sleep(1000); System.out.println(3); } }
-
futureTask创建一个有返回值得线程
package Demo1; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class Test4 { static class Task implements Callable<Integer>{ @Override public Integer call() throws Exception { Thread.sleep(2000); return 1; } } public static void main(String[] args) throws ExecutionException, InterruptedException { System.out.println(2); FutureTask<Integer> futureTask = new FutureTask<>(new Task()); System.out.println(3); new Thread(futureTask).start(); System.out.println(4); Integer integer = futureTask.get(); System.out.println(5); System.out.println(integer); } }
futureTask.get() 一旦调用这个方法,主线程会一直等待这个futureTask线程执行后再继续
守护线程
-
Java提供两种类型的线程:
用户线程
和守护程序线程
。守护线程为用户线程服务,一旦用户线程终止,守护线程立刻终止(QQ程序一旦直接终止,聊天窗口呀,qq宠物这些线程也会终止) -
守护线程的使用
package Demo1; import util.ThreadUtil; public class Test5 { public static void main(String[] args) { Thread t1=new Thread(()->{ int count=10; Thread t2=new Thread(()->{ while (true){ ThreadUtil.sleep(300); System.out.println("我是个守护线程"); } }); t2.setDaemon(true);//设置为守护线程 t2.start(); while (count>=0){ ThreadUtil.sleep(200); System.out.println("我是个用户线程"); count--; } System.out.println("用户线程结束"); }); t1.start(); } }
t1线程循环结束,t2线程也跟着结束
线程生命周期
-
在Thread类中有一个内部枚举类,表示生命周期,有图为更好地理解
public enum State { /** * Thread state for a thread which has not yet started. */ NEW, /** * Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may * be waiting for other resources from the operating system * such as processor. */ RUNNABLE, /** * Thread state for a thread blocked waiting for a monitor lock. * A thread in the blocked state is waiting for a monitor lock * to enter a synchronized block/method or * reenter a synchronized block/method after calling * {@link Object#wait() Object.wait}. */ BLOCKED, /** * Thread state for a waiting thread. * A thread is in the waiting state due to calling one of the * following methods: * <ul> * <li>{@link Object#wait() Object.wait} with no timeout</li> * <li>{@link #join() Thread.join} with no timeout</li> * <li>{@link LockSupport#park() LockSupport.park}</li> * </ul> * * <p>A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called <tt>Object.wait()</tt> * on an object is waiting for another thread to call * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on * that object. A thread that has called <tt>Thread.join()</tt> * is waiting for a specified thread to terminate. */ WAITING, /** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: * <ul> * <li>{@link #sleep Thread.sleep}</li> * <li>{@link Object#wait(long) Object.wait} with timeout</li> * <li>{@link #join(long) Thread.join} with timeout</li> * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li> * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li> * </ul> */ TIMED_WAITING, /** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED; }
状态 描述 【NEW】 这个状态主要是线程未被Thread.start()调用前的状态。 【RUNNABLE】 线程正在JVM中被执行,等待来自操作系统(如处理器)的调度。 【BLOCKED】 阻塞,因为某些原因不能立即执行需要挂起等待。 【WAITING】 无限期等待,由于线程调用了 Object.wait(0)
,Thread.join(0)
和LockSupport.park
其中的一个方法,线程处于等待状态,其中调用wait
,join
方法时未设置超时时间。【TIMED_WAITING】 有限期等待, 线程等待一个指定的时间,比如线程调用了 Object.wait(long)
,Thread.join(long)
,LockSupport.parkNanos
,LockSupport.parkUntil
方法之后,线程的状态就会变成TIMED_WAITING【TERMINATED】 终止的线程状态,线程已经完成执行。
目前我们可以学习一下join方法,他是这么用的:
package Demo1;
import util.ThreadUtil;
public class Test6 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
ThreadUtil.sleep(10);
System.out.println("这是线程1------"+i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
ThreadUtil.sleep(100);
System.out.println("这是线程2------"+i);
}
});
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("------------------");
}
}
研究主线程打印的虚线位置,join方法的本意是阻塞主线程,等待调用join的线程(这里会等待t1线程运行结束才会进行主线程)
线程安全的讨论
CPU多核缓存架构
CPU分为三级缓存: 每个CPU都有L1,L2缓存,但是L3缓存是多核公用的。
CPU查找数据的顺序为: CPU -> L1 -> L2 -> L3 -> 内存 -> 硬盘
从CPU到 | 大约需要的时间 |
---|---|
主存 | 60~80纳秒 |
L3 cache | 大约15纳秒 |
L2 cache | 大约3纳秒 |
L1 cache | 大约1纳秒 |
寄存器 | 大约0.3纳秒 |
-
CPU每次读取一个数据,并不是仅仅读取这个数据本身,而是会读取与它相邻的64个字节的数据,称之为【缓存行】,因为CPU认为,我使用了这个变量,很快就会使用与它相邻的数据,这是计算机的局部性原理。
-
可见性问题:一个线程修改的值对其他线程可能不可见 (比如两个CPU读取了一个缓存行,缓存行里有两个变量,一个x一个y。第一颗CPU修改了x的数据,还没有刷回主存,此时第二颗CPU,从主存中读取了未修改的缓存行,而此时第一颗CPU修改的数据刷回主存,这时就出现,第二颗CPU读取的数据和主存不一致的情况) 解决方案可通过标识状态(如英特尔的MESI协议)
-
资源争夺问题
期望是3,但是结果可能为2 解决方案用上锁,volatile修饰变量
- 除了增加高速缓存之外,为了使处理器内部的运算单元尽量被充分利用。处理器可能会对输入的代码进行【乱序执行】,化处理器会在计算之后将乱序执行的结果【进行重组】,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句的先后执行顺序与输入代码中的顺序一致。 JVM上成为指令重排的优化
JMM-java内存模型
在java内存模型当中一样会存在可见性和指令重排的问题。
JMM模型当中存在的问题
指令重排
存在依赖关系的不能进行重排,没有依赖关系的可以进行重排
代码验证指令重排的存在
package Demo2;
public class Test1 {
private static int x=0,y=0;
private static int a=0,b=0;
private static int count=0;
private static volatile int NUM=0;
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
for(;;){
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
a=1;
x=b;
}
});
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
b=1;
y=a;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("一共执行了:"+(count++)+"次");
if(x==0&&y==0){
long end = System.currentTimeMillis();
System.out.println("耗时:"+(end-start)+"毫秒,("+x+","+y+")");
break;
}
a=0;b=0;x=0;y=0;
}
}
}
执行顺序 | 结果 |
---|---|
[a = 1] —> [x = b] —> [b = 1] —> [y = a] | 0,1 |
[b = 1] —> [y = a]—> [a = 1] —> [x = b] | 1,0 |
[a = 1] —> [b = 1] —> [x = b] —> [y = a] | 1,1 |
[x = b] —> [y = a] —> [a = 1] —> [b = 1] | 0,0 |
因为指令重排的存在,所有结果都是有可能的
解决方案:使用内存屏障
硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
是在我们的读写操作之前加入一条指令,当CPU碰到这条指令后必须等到前边的指令执行完成才能继续执行下一条指令。
可见性
看以下的代码
package Demo2;
import util.ThreadUtil;
public class Test2 {
private static boolean isOver=false;
private static int number=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
while (!isOver){
}
System.out.println(number);
});
t1.start();
ThreadUtil.sleep(1000);
number=50;
isOver=true;
}
}
运行结果显示,一直在死循环中,永远看不到number的输出,原因在于t1线程一直在高速缓存中读取isOver的值,无法感知到主线程已经修改isOver的值而退出循环,这就是可见性的问题 使用关键字【volatile】关键字可以解决这个问题
private static volatile boolean isOver=false;
volatile 禁止指令重排 内存的可见性
线程争抢
多个线程对同一个值放入到自己的内存中进行处理并返回,抢夺同一个资源
举一个例子证明一下,线程1和线程2分别对count累计10000次,合适的结果应该是20000才对:
package Demo2;
public class Test3 {
private static int COUNT=0;
public static void adder(){
COUNT++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
adder();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
adder();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(COUNT);
}
}
运行结果是每次都是10000以上,但就是没有达到自己的期望值。
原因就是一个线程的结果对另一个线程不可见,修改了值,但另一个线程还在用旧值
线程争抢会产生数据安全问题,解决的最好方案就是【加锁】
public static synchronized void adder(){
COUNT++;
}
运行结果正确 20000
再举一个例子加锁理解,卖票
package Demo2;
import util.ThreadUtil;
public class Ticket implements Runnable{
private String name;
private static int COUNT=100;
public Ticket(String name) {
this.name = name;
}
@Override
public void run() {
while (COUNT>0){
ThreadUtil.sleep(100);
System.out.println(name+"出票一张,还剩"+Ticket.COUNT--+"张!");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new Ticket("一号窗口"));
Thread t2 = new Thread(new Ticket("二号窗口"));
t1.start();
t2.start();
}
}
我们加入synchronized同步代码块后,问题得到解决
package Demo2;
import util.ThreadUtil;
public class Ticket implements Runnable{
private static final Object monitor=new Object();
private String name;
private static int COUNT=100;
public Ticket(String name) {
this.name = name;
}
@Override
public void run() {
while (COUNT>0){
ThreadUtil.sleep(100);
synchronized (Ticket.monitor){
System.out.println(name+"出票一张,还剩"+Ticket.COUNT--+"张!");
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new Ticket("一号窗口"));
Thread t2 = new Thread(new Ticket("二号窗口"));
t1.start();
t2.start();
}
}
线程安全的实现方法概况
数据不可变
一切不可变的对象一定是数据安全的,如final修饰的基础数据类型,JAVA字符串,谷歌的开发工具包(guava)中也给我们提供了一些不可变的一类(immutable),后面会用到
互斥同步
同步是指同一个时刻只能由一个线程进行访问,互斥是同步实现的手段,互斥是方法,同步是目的,通过
synchronized
字段或者通过加锁来实现互斥同步
非阻塞同步
互斥同步也叫阻塞同步,会进行线程阻塞和唤醒的开销,属于一种【悲观的并发策略】(认为自己的下一步肯定会出现问题,所以不去执行)。而非阻塞同步是一种基于【冲突检测的乐观并发策略】,不管风险先进行操作,若尝试冲突,则进行补偿策略(不断的重试)
无同步方案
把共享的数据限制在一个线程中,无需同步,就不会产生多线程的数据安全问题(我用我的,你用你的)常见的解决方案就是ThreadLocal
ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
package Demo2;
public class Test4 {
private static int number=0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("t1----" + number++);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("t2----" + number++);
}
});
t1.start();
t2.start();
}
}
使用ThreadLocal改造
package Demo2;
public class Test4 {
private final static ThreadLocal<Integer> number = new ThreadLocal<>();
public static final int COUNT = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
number.set(COUNT);
for (int i = 0; i < 1000; i++) {
number.set(number.get() + 1);
System.out.println("t1----:" + number.get());
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
number.set(COUNT);
for (int i = 0; i < 1000; i++) {
number.set(number.get() + 1);
System.out.println("t2----:" + number.get());
}
}
});
t1.start();
t2.start();
}
}
输出结果就是 t1自己加到1000,t2自己加到1000
锁机制
synchronized简介
三种方式来加锁
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
- 静态方法,做用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码前要获得给定对象的锁
使用方法如下:
分类 | 具体分类 | 被锁对象 | 伪代码 |
---|---|---|---|
方法 | 实例方法 | 调用该方法的实例对象 | public synchronized void method(){ } |
方法 | 静态方法 | 类对象Class对象 | public static synchronized void method(){ } |
代码块 | this | 调用该方法的实例对象 | synchronized(this){ } |
代码块 | 类对象 | 类对象 | synchronized(Demo.class){ } |
代码块 | 任意的实例对象 | 创建的任意对象 | Object lock= new Object(); synchronized(lock){ } |
synchronizd原理分析
首先编写一段简单代码,然后找到同级目录下,用javap -v Test1.class查看字节码文件
反编译后的指令能看到
monitorenter
和monitorexit
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: ldc #2 // class Demo3/Test1
2: dup
3: astore_1
4: monitorenter
5: iconst_1
6: istore_2
7: aload_1
8: monitorexit
9: goto 17
12: astore_3
13: aload_1
14: monitorexit
15: aload_3
16: athrow
17: return
}
从中可以看出,线程获取锁的时候,实际上就是获得一个监视器对象(monitor),所有Java对象天生携带mmonitor。而monitor是添加Synchronized关键字之后独有的, 线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,而执行monitorexit,就是释放monitor的所有权
锁升级中涉及的四把锁:
- 无锁:不加锁
- 偏向锁:不锁锁,只有一个线程争夺时,偏心某一个线程,这个线程来了不加锁
- 轻量级锁: 少量线程来了之后,先尝试自旋,不挂起线程
- 重量级锁:排队挂起线程
Mark Word与锁
JVM一般是这样使用锁和Mark Word的:
1,当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
2,当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
3,当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
4,当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
5,偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在【当前线程】的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,也叫所记录(lock record),同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
6,轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7,自旋默认10次。
7,自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞排队。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等)进入阻塞状态,等待将来被唤醒。就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源。
死锁
死锁:多个线程同时被阻塞,都在等待某个资源被释放。由于线程被无限期的阻塞,程序因此不可能正常终止
Java死锁产生的四个条件
- 互斥使用,当一个资源被线程占有时,其他线程不能使用
- 不可抢占,资源请求者不能强制剥夺占有者的资源,资源只能通过占有者自主释放
- 请求和保持,线程在请求新资源的同时保持对原有资源的占有
- 循环等待,形成环路,t1等t2,t2等t1,构成一个等待环路
上述四个条件都成立时,便形成死锁,打破上述条件中的一个,都能打破死锁的现象
死锁问题方法两种 1.synchronized 2.Lock显示锁
package Demo3;
import java.util.Date;
public class Test2 {
public static String obj1="obj1";
public static String obj2="obj2";
static class LockA implements Runnable{
@Override
public void run() {
try{
System.out.println(new Date().toString()+"LockA 开始执行");
while (true){
synchronized (Test2.obj1){
System.out.println(new Date().toString()+"LockA 锁住 obj1");
Thread.sleep(3000);//此处等待是给B能锁住机会
synchronized (Test2.obj2) {
System.out.println(new Date().toString() + " LockA 锁住 obj2");
Thread.sleep(60 * 1000); // 为测试,占用了就不放
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
static class LockB implements Runnable {
public void run() {
try {
System.out.println(new Date().toString() + " LockB 开始执行");
while (true) {
synchronized (Test2.obj2) {
System.out.println(new Date().toString() + " LockB 锁住 obj2");
Thread.sleep(3000); // 此处等待是给A能锁住机会
synchronized (Test2.obj1) {
System.out.println(new Date().toString() + " LockB 锁住 obj1");
Thread.sleep(60 * 1000); // 为测试,占用了就不放
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
LockA lockA = new LockA();
new Thread(lockA).start();
LockB lockB = new LockB();
new Thread(lockB).start();
}
}
LockA占有obj1,想要obj2,而LockB占有obj2,想要obj1
就像两个小孩子吵架,互相拿了对方的玩具,大家归还时都说你先给我,我就给你,这样谁都拿不到
线程重入
指的是任意线程获得锁之后,再次获得该锁时不再会被该锁阻塞
package Demo3;
public class Test3 {
public static final Object M1=new Object();
public static final Object M2=new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (M1){
synchronized (M2){
synchronized (M1){
synchronized (M2){
System.out.println("hello lock");
}
}
}
}
}).start();
}
}
这就是线程重入,synchronized是可重入锁
wait和notify
看以下的例子
package Demo3;
import util.ThreadUtil;
public class Test4 {
private static final Object MONITOR = new Object();
public static void main(String[] args) {
new Thread(()->{
try{
synchronized (MONITOR){
System.out.println( "t1:task start, wait for notify...");
MONITOR.wait();
System.out.println("t1 task continue");
}
}catch (Exception e){
e.printStackTrace();
}
}).start();
ThreadUtil.sleep(3000);
synchronized (MONITOR){
MONITOR.notify();
}
}
}
线程一自动阻塞等待,等3秒主线程把锁激活,唤醒线程
Thread的两个静态方法:
Thread.sleep()释放CPU资源,但不释放锁
Thread.yield 释放执行权,但依然保留cpu执行资格(==我放弃执行,但仍然与你争取cpu)
package Demo3;
import java.util.concurrent.atomic.AtomicInteger;
public class Test5 {
private static AtomicInteger T1_COUNT=new AtomicInteger();
private static AtomicInteger T2_COUNT=new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
Thread.yield();
T1_COUNT.getAndAdd(1);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
T2_COUNT.getAndAdd(1);
}
});
t1.start();
t2.start();
t2.join();
System.out.println("t1执行了:"+T1_COUNT.get());
System.out.println("t2执行了:"+T2_COUNT.get());
}
}
我们不能要t1.join()
,因为要看相同时间内t1与t2的执行次数
t1执行了:255
t2执行了:1000
当我们注释了yield方法
// Thread.yield();
t1执行了:1000
t2执行了:1000
线程实例的方法:
- join:是线程的方法,程序会阻塞在这里等着这个线程执行完毕,才接着向下执行。
Object的成员方法
- wait:释放CPU资源,同时释放锁。
- notify:唤醒等待中的线程。
- notifyAll:唤醒所有等待的线程。
线程的退出
-
使用退出标志,是线程正常退出,就是当
run
方法结束后线程终止package Demo3; import util.ThreadUtil; public class Test6 { private static volatile boolean flag=true;//使用volatile解决线程的可见性问题 public static void main(String[] args) { Thread t1 = new Thread(() -> { while (flag) { System.out.println("线程一直运行"); } System.out.println("线程退出"); }); t1.start(); ThreadUtil.sleep(3000); flag=false; } }
-
使用
interrupt()
方法中断进程(会抛出InterrException
异常)package Demo3; public class Test7 { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { System.out.println("线程开始工作"); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程结束工作"); }); t1.start(); Thread.sleep(3000); t1.interrupt();//打断t1线程睡眠 } }
如果线程处于类似
while(true)
运行的状态,interrupt()
方法无法中断线程。
LockSupport
LockSupport
是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。
接下来我来看看LockSupport
有哪些常用的方法。主要有两类方法:park
和unpark
public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);
park就是停车的意思,把线程当做车辆,park就是让他停下,unpark就是让车启动
package Demo3;
import util.ThreadUtil;
import java.util.concurrent.locks.LockSupport;
public class Test8 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("线程1开始工作");
LockSupport.park();
if (Thread.currentThread().isInterrupted()) {
System.out.println("线程被中断了");
}
System.out.println("线程1继续执行");
});
t1.start();
ThreadUtil.sleep(2000);
LockSupport.unpark(t1);//唤醒t1线程
}
}
这儿park
和unpark
其实实现了wait
和notify
的功能,不过还是有一些差别的。
-
park
不需要获取某个对象的锁 -
因为中断的时候
park
不会抛出InterruptedException
异常,所以需要在park
之后自行判断中断状态,然后做额外的处理
我们在park线程的时候可以传递一些信息,给调用者看,参数是object,所以什么都能传递。
LockSupport.park("我被阻塞了");
System.out.println(LockSupport.getBlocker(t1));//获取传入信息
Lock锁
lock接口的几个方法
// 获取锁
void lock()
// 仅在调用时锁为空闲状态才获取该锁,可以响应中断
boolean tryLock()
// 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁
boolean tryLock(long time, TimeUnit unit)
// 释放锁
void unlock()
获取锁的两种方法
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
Lock的实现类ReetrantLock 可重入锁
对卖票代码进行改写
package Demo3;
import util.ThreadUtil;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable{
private static final Lock lock=new ReentrantLock();
private String name;
private static int COUNT=100;
public Ticket(String name) {
this.name = name;
}
@Override
public void run() {
while (COUNT>0){
ThreadUtil.sleep(100);
lock.lock();
try {
System.out.println(name+"出票一张,还剩"+Ticket.COUNT--+"张!");
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new Ticket("一号窗口"));
Thread t2 = new Thread(new Ticket("二号窗口"));
t1.start();
t2.start();
}
}
synchronized和ReentrantLock的区别
- Lock是一个接口,synchronized是Java中的关键字,synchronized是内置的语言实现;
- synchronized发生异常时,会自动释放线程占用的锁,故不会发生死锁现象。Lock发生异常,若没有主动释放,极有可能造成死锁,故需要在finally中调用unLock方法释放锁;
- Lock可以让等待锁的线程响应中断,使用synchronized只会让等待的线程一直等待下去,不能响应中断
- Lock可以提高多个线程进行读操作的效率
Lock另一个实现类ReadWriteLock
读写锁给我们提供了一种锁,读的时候可以很多线程同时读,但是不能有线程写,写的时候是独占的,其他线程既不能写也不能读。在某些场景下能极大的提升效率。
package Demo3;
import util.ThreadUtil;
import java.util.Random;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test9 {
private static ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
private static int COUNT=1;
public static void main(String[] args) {
Runnable read=()->{
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
readLock.lock();
try {
ThreadUtil.sleep(2000);
System.out.println("我在读数据:"+COUNT);
}finally {
readLock.unlock();
}
};
Runnable write=()->{
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
writeLock.lock();
try {
ThreadUtil.sleep(2000);
System.out.println("我在写数据:"+COUNT++);
}finally {
writeLock.unlock();
}
};
for (int i = 0; i < 100; i++) {
Random random = new Random();
int flag=random.nextInt(100);
if(flag>20){
new Thread(read).start();
}else{
new Thread(write).start();
}
}
}
}
可以根据输出结果得知读写锁的原理
lock锁的原理cas和aqs
并发编程的三大特性
- 原子性 顺序不可打乱,不可中断,原子操作
- 可见性 一个线程对值的修改另一个线程无法感知
- 有序性
volatile:可以保证可见性和有序性
synchronized和Lock:可以保证原子性、可见性、有序性
CAS
CAS,comapre and swap (jdk11后改成compare and set )
给一个元素赋值得时候,先看内存里的这个值是否没变,没变我才修改,无锁操作,不需要挂起线程,先尝试如果失败,继续尝试,这样在少量竞争的情况下很大程度提升性能
CAS在底层中做的三个动作【取值】,【比较】,【赋值】这三个动作是CPU原语操作
Java中的CAS在Unsafe类实现
/**
*Object var1 你要修改哪个对象的成员变量
*long offset 这个值在这个对象中的偏移量
*Object expected 期望值
*Object x 实际值
*/
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
CAS保证的是对一个对象写操作的无锁原子性,加syncronized的也具有原子性。
CAS的缺点:
- ABA问题。当第一个线程执行CAS操作,尚未修改为新值之前,内存中的值已经被其他线程连续修改了两次,使得变量值经历 A -> B -> A的过程。绝大部分场景我们对ABA不敏感。解决方案:添加版本号作为标识,每次修改变量值时,对应增加版本号; 做CAS操作前需要校验版本号。JDK1.5之后,新增AtomicStampedReference类来处理这种情况。
- 循环时间长开销大。如果有很多个线程并发,CAS自旋可能会长时间不成功,会增大CPU的执行开销。
- 只能对一个变量进行原子操作。JDK1.5之后,新增AtomicReference类来处理这种情况,可以将多个变量放到一个对象中。
AQS
AQS即AbstractQueuedSynchronizer
抽象队列同步器,用来解决线程同步执行的问题
解决问题思路如下:
在源码中看到AQS维护一个双向队列,队列中保存一个线程,还有一个状态
该队列为CLH队列
以ReentrantLock
分析其中过程
有一个sync
abstract static class Sync extends AbstractQueuedSynchronizer
两个实现类 公平锁和不公平锁
static final class FairSync extends Sync
static final class NonfairSync extends Sync
-
构造 不传值,默认是非公平锁,传入true是公平锁
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
-
加锁 (获取锁)acquire就是获取的意思:
// NonfairSync 非公平的加锁动作一上来就抢一下,这是非公平锁的第一次抢锁 final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } // FairSync 公平锁直接调用acquire(1) final void lock() { acquire(1); }
sync.acquire(1) 方法:
public final void acquire(int arg) { if ( !tryAcquire(arg) && acquireQueued( addWaiter(Node.EXCLUSIVE), arg) ) selfInterrupt(); }
尝试获得锁
tryAcquire(arg)
非公平锁的获取方式
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 直接设置状态,并将当前的锁持有者改成自己,第二次自旋获取,非公平锁有两次抢锁的机会
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁的获取方式
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 先看看有没有排队的节点,再尝试获取锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
公平锁 会看看有没有队列,有队列就排队,而非公平锁根本不管有无队列都直接抢锁
-
入队
没有获得锁,就排队,addWaiter(Node.EXCLUSIVE) 添加一个节点到队列
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
private Node enq(final Node node) { for (;;) { Node t = tail; // 插入了一个空节点,就是一个哨兵,因为每一个真实的线程节点都会坚挺前一个节点的状态 if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
-
阻塞
入队完成之后再判断一次当前是否有可能获得锁,也就是前一个节点是head的话,前一个线程有可能已经释放了,再获取一次,如果获取成功,设置当前节点为头节点,整个获取过程完成。
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); // 不死心,进了队伍了,发现我是第二个,还要尝试获取一下 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } // 真正的挂起线程 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
获取失败的话先将之前的节点等待状态设置为SIGNAL(状态为需要唤醒的状态),如果之前的节点取消了就向前一直找。
// 就是要将我的前一个节点的等待状态改为SIGNAL private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. * 前驱节点已经设置了SIGNAL,闹钟已经设好,现在我可以安心睡觉(阻塞)了。 * 如果前驱变成了head,并且head的代表线程exclusiveOwnerThread释放了锁, * 就会来根据这个SIGNAL来唤醒自己 */ return true; if (ws > 0) { /* * 发现传入的前驱的状态大于0,即CANCELLED。说明前驱节点已经因为超时或响应了中断, * 而取消了自己。所以需要跨越掉这些CANCELLED节点,直到找到一个<=0的节点 */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * 进入这个分支,ws只能是0或PROPAGATE。 * CAS设置ws为SIGNAL */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
上一个条件完成之后,我就可以安心的阻塞了,然后一直等待直到被唤醒
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
上面就是获取锁并等待的过程,总结起来就是:
当lock()
执行的时候:
- 先快速获取锁,当前没有线程执行的时候直接获取锁
- 尝试获取锁,当没有线程执行或是当前线程占用锁,可以直接获取锁
- 将当前线程包装为node放入同步队列,设置为尾节点
- 前一个节点如果为头节点,再次尝试获取一次锁
- 将前一个有效节点设置为SIGNAL(后续线程需要取消停放的状态)
- 然后阻塞直到被唤醒
-
释放锁
当ReentrantLock进行释放锁操作时,调用的是AQS的
release(1)
操作public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
在tryRelease(arg)
中会将锁释放一次,如果当前state是1,且当前线程是正在占用的线程,释放锁成功,返回true,否则因为是可重入锁,释放一次可能还在占用,应一直释放直到state为0为止
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 如果没有下一个节点,或者下个节点的状态被取消了,就从尾节点开始找,找到最前面一个可用的节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 唤醒下一个节点
if (s != null)
LockSupport.unpark(s.thread);
}
JUC并发编程包
原子类
认识原子类
Atomic原子类,是具有原子操作特征的类,在java.util.concurrent.atomic
包下
JUC包中的原子类
基本类型
使用原子的方式更新基本类型
- AtomicInteger:整形原子类
- AtomicLong:长整型原子类
- AtomicBoolean:布尔型原子类
数组类型
使用原子的方式更新数组里的某个元素
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray:引用类型数组原子类
引用类型
- AtomicReference:引用类型原子类
- AtomicStampedReference:原子更新引用类型里的字段原子类
- AtomicMarkableReference :原子更新带有标记位的引用类
对象的属性修改类型
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,以及解决使用 CAS 进行原子更新时可能出现的 ABA 问题
AtomicIntenger的使用
打开AtomicIntenger
可查看该类常用方法
public final int get(); // 获取当前的值
public final int getAndSet(int newValue); // 获取当前的值,并设置新的值
public final int getAndIncrement(); // 获取当前的值,并自增
public final int getAndDecrement(); // 获取当前的值,并自减
public final int getAndAdd(int delta); // 获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update); // 如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue); // 最终设置为 newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
小demo测试
package Demo4;
import util.ThreadUtil;
import java.util.concurrent.atomic.AtomicInteger;
public class Test1 {
public static void main(String[] args) {
AtomicInteger ADDER=new AtomicInteger();
for (int i = 0; i < 100; i++) {
new Thread(()->{
ADDER.getAndAdd(1);
}).start();
}
ThreadUtil.sleep(2000);
System.out.println(ADDER.get());
}
}
开启100个线程做加法,没有加锁和关键字,保证了线程安全问题
实现原理
该类维护一个volatile
修饰的int,保证了可见性和有序性:
private volatile int value;
所有的方法都是使用cas
保证了原子性,所以这几个类都是线程安全的:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
我们发现原子类中的任何操作都没有上锁,是无锁操作。
线程池
将多个线程提前创建好,放入一个容器中
线程池的意义:
- 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。 当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。
JDK自带的四种线程池
Java通过Executors提供四种线程池,分别为:
-
newCachedThreadPool‘
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 -
newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 -
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。 -
newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
简单使用
package Demo4;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test2 {
public static void main(String[] args) {
Runnable task=()->{
System.out.println(Thread.currentThread().getName()+": task One");
};
ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 40; i++) {
pool.submit(task);
}
}
}
线程池的构造方法
public ThreadPoolExecutor
(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
}
线程池参数意义
corePoolSize | 指定了线程池里的线程数量,核心线程池大小 |
---|---|
maximumPoolSize | 指定了线程池里的最大线程数量 |
keepAliveTime | 当线程池线程数量大于corePoolSize时候,多出来的空闲线程,多长时间会被销毁 |
unit | 时间单位,TimeUnit |
workQueue | 任务队列,用于存放提交但是尚未被执行的任务 |
threadFactory | 线程工厂,用于创建线程,线程工厂就是给我们new线程的 |
handler | 所谓拒绝策略,是指将任务添加到线程池中时,线程池拒绝该任务所采取的相应策略 |
常见的工作队列
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
- LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
线程池提供的拒绝策略
- AbortPolicy:直接抛出异常,默认策略;
- CallerRunsPolicy:用调用者所在的线程来执行任务;
- DiscardOldestPolicy:丢弃阻塞队列中最靠前的任务,并执行当前任务;
- DiscardPolicy:直接丢弃任务;
线程池执行任务的流程
查看四种线程池的构造
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
核心线程池大小=0
最大线程池大小为Integer.MAX_VALUE
线程过期时间为60s
使用SynchronousQueue作为工作队列.
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
核心线程池大小=传入参数
最大线程池大小为传入参数
线程过期时间为0ms
LinkedBlockingQueue作为工作队列.
newScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
核心线程池大小=传入参数
最大线程池大小为Integer.MAX_VALUE
线程过期时间为0ms
DelayedWorkQueue作为工作队列.
主要是通过DelayedWorkQueue来实现的定时线程。
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
核心线程池大小=1
最大线程池大小为1
线程过期时间为0ms
LinkedBlockingQueue作为工作队列.
自定义线程池
这里是针对JDK1.8版本,使用JDK自带的线程池会出现OOM内容溢出问题,中小型公司一般很难遇到,在阿里巴巴开发文档上面有明确的标识:
所以我们自定义线程池时,我们自己构造参数,弄自定义线程池:
- 核心线程数、最大线程数、活跃时间和单位根据服务器本身的性能和程序的特性设定,这个是个经验值
- 拒绝策略其实很简单,ExecutorService构造时可以不传递拒绝策略,默认使用异常抛出的方式。
- 阻塞队列我们搞一个定长的队列就好了,ArrayBlockingQueue<>(DEFAULT_SIZE)
- 线程工厂的获取我们可以接下来讲解
原生方式构造线程工厂
源码
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
package Demo4;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class MyThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
MyThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
许多工具包现在都有创建线程工厂,如guava , commons-lang3,hutool
我们用国产hutool来实现线程工厂创建(具体可参考文档)
要么引入依赖,要么引入lib包
package Demo4;
import cn.hutool.core.thread.ExecutorBuilder;
import util.ThreadUtil;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
public class Test3 {
public static void main(String[] args) {
ExecutorService executor = ExecutorBuilder.create()
.setCorePoolSize(5)
.setMaxPoolSize(10)
.setWorkQueue(new ArrayBlockingQueue<>(100))
.build();
Runnable task=()->{
System.out.println(Thread.currentThread().getName()+": task One");
};
for (int i = 0; i < 50; i++) {
executor.submit(task);
}
}
}
线程同步
这些类为JUC包,它们都起到线程同步作用
CountDownLatch (倒计时器)
这个类常常用于等待,等多个线程执行完毕,再让某个线程执行。
CountDownLatch的典型用法就是:某一线程在开始运行前等待n个线程执行完毕。
使用方法如下:
-
将 CountDownLatch 的计数器初始化为n :new CountDownLatch(n),
-
每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,
在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行
用多线程分开计算分公司账目,主线程最后计算总账
package Demo4;
import util.ThreadUtil;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test4 {
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newCachedThreadPool();
CountDownLatch countDownLatch = new CountDownLatch(3);
Runnable task1=()->{
ThreadUtil.sleep(new Random().nextInt(5000));
System.out.println("计算广州分公司账目");
countDownLatch.countDown();
};
Runnable task2=()->{
ThreadUtil.sleep(new Random().nextInt(5000));
System.out.println("计算深圳分公司账目");
countDownLatch.countDown();
};
Runnable task3=()->{
ThreadUtil.sleep(new Random().nextInt(5000));
System.out.println("计算北京分公司账目");
countDownLatch.countDown();
};
pool.submit(task1);
pool.submit(task2);
pool.submit(task3);
countDownLatch.await();
System.out.println("计算总账");
}
}
CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
CyclicBarrier(循环栅栏)
CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
看 如下示意图,CyclicBarrier 和 CountDownLatch 是不是很像,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic)。
一样的demo,用循环栅栏完成
package Demo4;
import util.ThreadUtil;
import java.util.Random;
import java.util.concurrent.*;
public class Test5 {
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newCachedThreadPool();
Runnable main=()->{
System.out.println("计算总账");
};
CyclicBarrier cyclicBarrier = new CyclicBarrier(3,main);
Runnable task1=()->{
ThreadUtil.sleep(new Random().nextInt(5000));
System.out.println("计算广州分公司账目");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
};
Runnable task2=()->{
ThreadUtil.sleep(new Random().nextInt(5000));
System.out.println("计算深圳分公司账目");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
};
Runnable task3=()->{
ThreadUtil.sleep(new Random().nextInt(5000));
System.out.println("计算北京分公司账目");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
};
pool.submit(task1);
pool.submit(task2);
pool.submit(task3);
// 重复利用
ThreadUtil.sleep(5000);
cyclicBarrier.reset();
System.out.println("-------------reset-----------");
pool.submit(task1);
pool.submit(task2);
pool.submit(task3);
}
}
CyclicBarrier与CountDownLatch的区别
至此我们难免会将CyclicBarrier与CountDownLatch进行一番比较。这两个类都可以实现一组线程在到达某个条件之前进行等待,它们内部都有一个计数器,当计数器的值不断的减为0的时候所有阻塞的线程将会被唤醒。
有区别的是CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制,在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值。
另外,CountDownLatch只能拦截一轮,而CyclicBarrier可以实现循环拦截。一般来说用CyclicBarrier可以实现CountDownLatch的功能,而反之则不能。总之,这两个类的异同点大致如此,至于何时使用CyclicBarrier,何时使用CountDownLatch,还需要读者自己去拿捏。
Semaphore(线程量)
相当于令牌,线程要获取线程量才能继续执行,没有获取则排队获取,执行成功后返回释放令牌
package Demo4;
import util.ThreadUtil;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class Test6 {
public static void main(String[] args) {
final Semaphore semaphore=new Semaphore(5);
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
Runnable run=()->{
try{
semaphore.acquire();
System.out.println("停进一辆车");
ThreadUtil.sleep(new Random().nextInt(5000));
semaphore.release();
System.out.println("开走一辆车");
}catch (Exception e){
e.printStackTrace();
}
};
pool.submit(run);
}
pool.shutdown();
}
}
最后的结果是开始五辆车全部进入,因为停车场是空的,后边就是出一辆进一辆
单例
懒汉模式 (当程序需要这个实例的时候才去创建对象,就如同一个人懒的饿到不行了才去吃东西)
线程不安全,延迟初始化(当你调用方法才进行初始化工作),严格意义上不是单例模式
public class Singleton {
private static Singleton instance;
private Singleton(){};
public static Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}
饿汉模式(不管程序是否需要这个对象的实例,总是在类加载的时候就先创建好实例,理解起来就像不管一个人想不想吃东西都把吃的先买好,如同饿怕了一样)
线程安全,比较常用,但容易产生垃圾,因为一开始就初始化
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
双重锁模式,双重检查(重点)
线程安全,延迟初始化。采用双锁机制,安全且在多线程情况下减少并发系统中竞争和同步的开销,能保持高性能。
public class Singleton {
// volatile如果不加可能会出现半初始化的对象
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
登记式/静态内部类
线程安全,这种方式能达到双检锁方式一样的功效,但实现更简单
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举
很少用
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
总结
一般建议使用饿汉式加载线程,如果有其他特殊需求,可以考虑使用双重锁的方式