初识Java多线程
一、创建多线程
多线程的作用:一个时间点,同时做多个事情
(一个时间点,同时执行多行代码)
java.lang.Thread
创建方法只有一种,new 一个 java.lang.Thread 类的对象
二、线程任务创建方法
使用java.lang.Thread的子类
new 一个Thread类的子类 重写run方法
- 使用匿名内部类
public class Main {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run () {
System.out.println("这是一个子线程");
}
};
}
}
- 使用静态内部类,该类继承自Thread类
static class A extends Thread {
@Override
public void run () {
System.out.println("这是一个子线程");
}
}
使用java.lang.Runable类
new 一个 java.lang.Runnable 子类 重写run方法,传入Thread构造函数中执行
public class Main {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("这是一个子线程");
}
};
Thread thread = new Thread(runnable, "线程1");
}
}
Thread thread = new Thread(runnable, “线程1”);
这里的第二个参数,是给该子线程起一个名字
使用java.util.concurrent.Callable接口
new 一个 java.util.concurrent.Callable 子类 重新call方法,再将该子类对象传入FutureTask的构造函数中 new 一个 java.util.concurrent.FutureTask 类的对象,然后将FutureTask类的对象传入Thread构造函数中执行
public static void main(String[] args) throws InterruptedException, java.util.concurrent.ExecutionException {
java.util.concurrent.Callable<String> callable = new java.util.concurrent.Callable<String>() {
@Override
public String call () {
long id = Thread.currentThread().getId();
System.out.println(id);
return "" + id;
}
};
java.util.concurrent.FutureTask futureTask = new java.util.concurrent.FutureTask(callable);
Thread thread = new Thread(futureTask);
}
三种创建任务的区别
前两种都是创建没有返回值的线程
最后一种是创建了带返回值的线程,在main线程中可以获取到这个返回的值
三、对线程操作
启动一个线程
启动线程使用 start() 方法
public class Main {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("这是一个子线程");
}
};
Thread thread = new Thread(runnable, "线程1");
//启动线程
thread.start();
}
}
线程的执行顺序
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("这是一个子线程");
}
};
Thread thread = new Thread(runnable, "线程1");
thread.start();
System.out.println("main线程执行");
上面这段代码,执行顺序是:
main线程的打印语句执行的概率大于子线程打印语句执行的概率
因为创建子线程比较耗时
Thread类的部分方法
1.public static Thread currentThread()
返回当前正在执行的线程对象的引用
2.public static void yield()
线程让步
即让当前线程由运行状态转变为就绪状态
3.public static void sleep(long millis) throws InterruptedException
使当前正在执行的线程在指定的毫秒数内休眠(暂时停止执行)
更多的方法请大家参考官方文档
链接: java16参考文档.
中断一个线程
1. 使用自定义标志位中断
public class Main {
//标志位
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
while (!flag) {
System.out.println("这是一个子线程");
Thread.sleep(5000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable, "线程1");
thread.start();
Thread.sleep(3000);
flag = true;
System.out.println("main线程执行");
}
}
2. 调用 interrupt() 函数直接中断线程
public class Main {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (Thread.currentThread().isInterrupted() == false) {
System.out.println("这是一个子线程");
}
}
};
Thread thread = new Thread(runnable, "线程1");
thread.start();
Thread.sleep(1000);
thread.interrupt();
System.out.println("main线程执行");
}
}
Thread类中有一个中断标志位,通过调用 interrupt() 函数就会让中断标志位变成 true,此时调用 isInterrupted() 方法获取中断标志位的值,通过条件判断使线程中断
等待一个线程
通过调用 join 方法
public class Main {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
};
Thread thread = new Thread(runnable, "线程1");
thread.start();
thread.join();
System.out.println("main线程执行");
}
}
执行结果
0
1
2
3
4
main线程执行
在等待子线程运行时,main线程,会卡在join()方法的调用处,直到子线程结束后,main线程才会向下运行
当然join也有参数
public final void join(long millis) throws InterruptedException
等待线程多少毫秒,到了时间后就不等待继续执行了,如果在时间之内子线程运行结束,那么就直接向下运行
获取线程返回值
只有用 java.util.concurrent.Callable 创建任务的方式创建线程,才可以使线程拥有返回值
public static void main(String[] args) throws InterruptedException, java.util.concurrent.ExecutionException {
java.util.concurrent.Callable<String> callable = new java.util.concurrent.Callable<>() {
@Override
public String call () {
long id = Thread.currentThread().getId();
System.out.println(id);
return "" + id;
}
};
java.util.concurrent.FutureTask futureTask = new java.util.concurrent.FutureTask(callable);
Thread thread = new Thread(futureTask);
thread.start();
String v = (String) futureTask.get();
System.out.println("线程返回值:" + v);
System.out.println("main线程id" + Thread.currentThread().getId());
}
执行结果
子线程打印线程id:18
main线程获取返回值:18
main线程打印id:1
使用 get() 方法可以获取返回值,该方法的返回值是 Object类对象 这里需要强制类型转换
String v = (String) futureTask.get();
注意,当main线程执行这行到这句代码时,main线程会卡在这里,直到获取到子线程的返回值,即直到子线程执行return语句,才会继续向下执行
四、线程状态
public static enum State {
NEW, //创建
RUNNABLE, //可运行
BLOCKED, //阻塞
WAITING, //等待
TIMED_WAITING,//超时等待
TERMINATED; //终止
private State() {
}
}
上面是 Thread.State 枚举类
里面是线程的六个状态
五、线程安全
由于多线程之间可以相互共享资源(变量),所以会出现一些问题,这些问题就是线程安全问题
不安全的原因:多个线程对同一个变量的操作
如:两个线程同时修改同一个变量的值,这就会导致变量的值不会出现我们预期的结果
public class Main {
private static int x = 0;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
x++;
}
}
};
Thread thread1 = new Thread(runnable, "线程1");
Thread thread2 = new Thread(runnable, "线程2");
thread1.start();
thread2.start();
while (Thread.activeCount() > 1)
Thread.yield();
System.out.println(x);
}
}
上面代码的结果每次都不一样,有的时候是20000,有的时候不是20000,这就是线程不安全引起的原因
不安全的原因之一是:不具有原子性
原子性是指:多行代码执行时,是不可再分的
举个例子,就好比你打开你的笔记本电脑,先翻开笔记本,再按电源键,这两个事情必须连在一起执行,不能分开,原子性也类似,对变量进行操作,必须一次性完成,不可分
解决方法
加锁共享变量
举个简单的例子:
共享变量就好比打印机,线程好比人,两个人同时使用一个打印机,肯定不行,会导致两个人要打印的内容打印到同一张纸上,为了防止这个现象的发生,那就要规定一个打印机在被一个人使用的时候,另一个人不能使用,这就相当于使用打印机的人给打印机加了锁,自己在使用时,其他人是不能使用的,对于线程和共享变量,也是这个道理
多个线程访问共享变量,最终表现为:
一个线程加锁,操作变量,释放锁,其他线程加锁失败,需要等待(等待可以是一直等着,也可以是执行其他代码,过一会再回来再尝试加锁)
这就好比一个人正在用打印机,你也想用,但是已经被占了,要么你就排队,排在他后面,等着他用完你用,此时你只能等着,做不了其他事;要么你先做其他事情,过一会来看一次,看一看打印机有人用没,如果有人用,就继续做其他事,没人用就使用打印机
锁,在java层面是一个对象,多个线程加锁,是对同一个对象加锁,解锁也是
四、加锁
synchronized 关键字
使用方法:
1. 静态方法:加锁整个方法,锁对象为类对象
public class Main {
private static int x = 0;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
add();
}
}
};
Thread thread1 = new Thread(runnable, "线程1");
Thread thread2 = new Thread(runnable, "线程2");
thread1.start();
thread2.start();
while (Thread.activeCount() > 1)
Thread.yield();
System.out.println(x);
}
private synchronized static void add() {
x++;
}
}
add() 方法是静态方法
2. 实例方法:加锁整个方法,锁对象为this
public class Main {
private static int x = 0;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
private synchronized void increase() {
x++;
}
};
Thread thread1 = new Thread(runnable, "线程1");
Thread thread2 = new Thread(runnable, "线程2");
thread1.start();
thread2.start();
while (Thread.activeCount() > 1)
Thread.yield();
System.out.println(x);
}
}
increase() 方法是实例方法
3. 同步代码块
public class Main {
private static int x = 0;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (o) {
x++;
}
}
}
Object o = new Object();
};
Thread thread1 = new Thread(runnable, "线程1");
Thread thread2 = new Thread(runnable, "线程2");
thread1.start();
thread2.start();
while (Thread.activeCount() > 1)
Thread.yield();
System.out.println(x);
}
}
具体用法:
synchronized (锁对象) {
共享变量操作;
}
当然也可以对一个锁反复加锁
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (o) {
synchronized (o) {
synchronized (o) {
synchronized (o) {
x++;
}
}
}
}
}
}
Object o = new Object();
};
volatile 关键字
不需要加锁即可保证线程安全性
用于保证了原子性的变量
读取变量值的操作:原子性保证了
修改变量值的操作:变量值不依赖共享变量,才保证了原子性
如:用常量给共享变量赋值,算保证了原子性
保证了线程的安全情况下,要尽可能的提高效率,这就需要我们加锁的时候要注意了,只加锁访问共享变量的代码,不访问共享变量的代码不要加锁
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (o) {
System.out.println("子线程操作变量");
x++;
}
}
}
Object o = new Object();
};
上面这段代码,实际上
System.out.println(“子线程操作变量”);
不需要加锁
举个例子,就好比打印机在一个房间内,房间内有饮水机等其他设备,你在使用打印机的时候,就不需要对整个房间加锁,你使用的是打印机,并不是房间内的其他设备,万一有人想喝水了,还得等你用完打印机,这就很影响其他人,线程之间也是
所以加锁一定要锁对代码
java.util.concurrent.locks.ReentrantLock 锁
比起 synchronized 加锁,该锁更为灵活,是直接创建了一个锁,需要手动加锁和解锁
new 一个 ReentrantLock 对象
public class Main {
private static int count = 0;
public static void main(String[] args) throws InterruptedException, java.util.concurrent.ExecutionException {
java.util.concurrent.locks.ReentrantLock mutex = new java.util.concurrent.locks.ReentrantLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
mutex.lock();
try {
for (int i = 0; i < 10000; i++) {
count++;
}
} finally {
mutex.unlock();
}
}
};
Thread thread1 = new Thread(runnable, "线程1");
Thread thread2 = new Thread(runnable, "线程2");
thread1.start();
thread2.start();
while (Thread.activeCount() > 1)
Thread.yield();
System.out.println(count);
}
}
加锁
lock()
解锁
unlock()
尝试加锁
tryLock()
判断是否加锁
isLocked()
public static void main (String[] args) throws InterruptedException {
java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock();
Runnable runnable1 = new Runnable() {
@Override
public void run() {
System.out.println("无限循环线程");
lock.lock();
try{
while (true)
;
} finally {
lock.unlock();
}
}
};
Thread thread1 = new Thread(runnable1,"线程1");
thread1.start();
Thread.sleep(3000);
if (lock.tryLock()) {
System.out.println("尝试加锁成功");
} else {
System.out.println("尝试加锁失败");
}
if (lock.isLocked()) {
System.out.println("已加锁");
} else {
System.out.println("未加锁");
}
}
结果:
无限循环线程
尝试加锁失败
已加锁
六、简单的单例设计模式
class Singleton {
private static volatile Singleton instance = null;
private Singleton () { }
public static Singleton getInstance () {
if (instance == null)
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton();
}
return instance;
}
}
这里说一下需要注意的几点
if (instance == null)
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton();
}
这里使用了双重校验锁(两个if语句)
为了防止 new 出来的对象是不同的
七、线程通信
线程并发执行时,可能会需要两个线程进行通信
举个例子:
我雇了一两个人,一个人帮我去买菜,另一个人帮我去存钱,但买菜的人到了菜市场,发现卡里没钱,此时就需要等待存钱的人把钱存上,然后通知买菜的人钱存上了。当卡里没钱时,这个买菜的人可以一直在菜市场买菜的地方等着存钱的人给他打电话说存好了,或者可以先做别的事情,等到存钱的人打电话通知钱存上了,再去买菜。
这个例子中,存钱的人就需要通知买菜的人,即一个线程通知另一个线程
线程通信相关方法
1. public final void notify()
唤醒正在等待的线程
锁对象.notify()
2. public final void wait() throws InterruptedException
使当前线程等待,直到另一个线程调用 notify() 方法或者 notifyAll() 方法
锁对象.wait()
3. public final void notifyAll()
唤醒所有正在等待的线程
这些方法的使用前提:
必须在 synchronized 作用的代码块内
必须都是对同一锁对象操作
下面是线程通信的例子
面包店:
- 1.生产者(面包师傅-线程):5个师傅,每个每次生产5个
- 2.消费者(线程):10个,每个每次消费2个
- 库存(共享变量):下限0,上限100
简单可以理解为:
面包师傅生产面包,消费者购买面包,5个面包师傅每次每人生产1个面包,10个消费者每次每人购买两个面包,每当面包被生产或者被购买时,消费者就要告诉面包师傅生产面包,面包师傅要告诉消费者购买面包,当面包数量不够1个时,消费者会等待,直到面包被生产到数量大于1个;当面包数量超过100个时,面包师傅将不会再生产面包,直到面包已经不够100个了。
public class BreadShop {
//库存
private static int COUNT;
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {//面包师傅
@Override
public void run() {
try {
while (true){//不停的生产面包
//面包师傅之间,面包师傅和消费者之间,都是对库存共享变量的操作,需要保证线程安全
synchronized (BreadShop.class){
//当前库存+当次生产数量如果超过库存上限,需要等待
while (COUNT > 95)
BreadShop.class.wait();
//满足生产条件,生产面包
COUNT += 5;
//生产后,需要通知消费者线程
BreadShop.class.notifyAll();
System.out.println(Thread.currentThread().getName()+"生产了面包,库存:" + COUNT);
}
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "面包师傅["+i+"]").start();
}
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {//消费者
@Override
public void run() {
try {
while (true){//不停的消费面包
//面包师傅之间,面包师傅和消费者之间,都是对库存共享变量的操作,需要保证线程安全
synchronized (BreadShop.class){
//当前库存-当次消费数量如果小于库存下限,需要等待
while (COUNT == 0 || COUNT == 1)
BreadShop.class.wait();
//满足消费条件,消费面包
COUNT -= 2;
//消费后,需要通知面包师傅线程
BreadShop.class.notifyAll();
System.out.println(Thread.currentThread().getName()+"消费了面包,库存:"+COUNT);
}
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者["+i+"]").start();
}
}
}
注意一点,调用wait()方法处,要在while循环里面
八、阻塞式队列
作用:
线程直接可以不用直接对数据操作,使用队列的方式间接操作数据
可以理解为:
面包师傅不把生产的面包放到仓库里面了,而是放到一个传送带上,消费者直接拿传送带上的面包即可,消费者拿面包,相当于出队列,面包师傅生产面包相当于入队列
实际上跟刚才的方式差不多,只是换了一种方法,刚才那种方法,生产出来的面包会直接被面包堆里,顾客会从面包堆里面拿,是无序的,也就是顾客拿到的面包不一定就是刚出炉的,也能是拿到了出炉很久的,而通过队列的方式拿面包,只有先把之前出炉的都拿走了,才能拿刚出炉的
九、线程池 java.util.concurrent.ThreadPoolExecutor
举个例子来理解线程池
面包店的老板开通了美团,可以网上送餐
面包店老板决定雇佣几个外面小哥帮送面包,小哥空闲时就休息,有订单时就送订单
这里,线程池就类似于外卖小哥,线程池里的线程被创建后,有任务了就执行任务,没任务就等待
创建线程池
ThreadPoolExecutor 类的构造方法(其中的一个)
public ThreadPoolExecutor(
int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //最大空闲时间
java.util.concurrent.TimeUnit unit, //时间单位
java.util.concurrent.BlockingQueue<Runnable> workQueue, //线程创建方式
java.util.concurrent.ThreadFactory threadFactory, //阻塞式队列
java.util.concurrent.RejectedExecutionHandler handler //拒绝策略
)
public static void main(String[] args) throws InterruptedException, java.util.concurrent.ExecutionException {
java.util.concurrent.ThreadPoolExecutor poolExecutor = new java.util.concurrent.ThreadPoolExecutor(
4,//核心线程数
10,//最大线程数
60,//最大空闲时间
java.util.concurrent.TimeUnit.SECONDS,//时间单位:秒
new java.util.concurrent.LinkedBlockingQueue<Runnable>(),//阻塞式队列
new java.util.concurrent.ThreadFactory() {
@Override
public Thread newThread (Runnable runnable) {
return new Thread(runnable);
}
},//线程创建方式
new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy()//拒绝策略:谁提交谁执行
);
}
向线程池中提交任务
1.使用 execute()方法
public void execute(Runnable command)
command 是要执行的任务
public static void main(String[] args) throws InterruptedException, java.util.concurrent.ExecutionException {
java.util.concurrent.ThreadPoolExecutor poolExecutor = new java.util.concurrent.ThreadPoolExecutor(
4,
10,
60,
java.util.concurrent.TimeUnit.SECONDS,
new java.util.concurrent.LinkedBlockingQueue<Runnable>(),
new java.util.concurrent.ThreadFactory() {
@Override
public Thread newThread (Runnable runnable) {
return new Thread(runnable);
}
},
new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy()
);
Runnable task1 = new Runnable() {
@Override
public void run() {
System.out.println("任务开始执行...");
}
};
poolExecutor.execute(task1);
}
2.使用 submit() 方法
public Future submit(Callable task)
task是有返回值的任务,通过submit方法的返回值可以获得线程任务结束的返回值
线程池的创建方法跟上面的一样,下面省略了
java.util.concurrent.Callable<Integer> callable = new java.util.concurrent.Callable(){
@Override
public Integer call () {
System.out.println("任务执行");
return 5;
}
};
java.util.concurrent.Future<Integer> future = poolExecutor.submit(callable);
int result = future.get();
System.out.println(result);
3.使用 submit()方法
public Future<?> submit(Runnable task)
该方法提交一个Runnable任务执行,并返回表示该任务的Future。Future的get方法将在成功完成时返回null
Runnable task1 = new Runnable() {
@Override
public void run() {
System.out.println("任务开始执行...");
int x = 0;
for (int i = 0; i < 100000; i++)
x++;
System.out.println(x);
}
};
java.util.concurrent.Future future = poolExecutor.submit(task1);
if (future.get() == null) {
System.out.println("任务结束");
}
System.out.println("main");
结果:
任务开始执行…
100000
任务结束
main
从上面代码可以看出,调用 get() 方法时,main线程会卡在调用的位置,直到任务执行完毕,所以get方法的返回值只有null,表示任务执行完毕,如果没执行完毕,直到线程结束,get方法才调用完成
使用Runnable的方式创建任务,任务没有返回值,所以使用get方法返回为null
关闭线程池
1.使用 shutdownNow() 方法
public List shutdownNow()
停止阻塞队列中还未执行的任务,正在执行的任务不会停止
返回值:未执行的任务的链表
2.使用 shutdown() 方法
public void shutdown()
终止所有任务,阻塞队列中没执行的任务和正在执行的任务都被终止
3.判断线程池是否关闭
public boolean isShutdown()
如果已关闭,则返回 true
线程池执行流程
java创建线程池,开始是没有线程的,每当任务来了才创建线程
用一个例子来说明线程池执行流程
面包店网上订面包,店主决定雇佣4个外卖小哥送外卖,当无订单时,店主不雇佣外卖小哥,当有订单时,店主开始雇佣长期的外卖小哥,但是不能超过四个,这四个长期雇佣的外面小哥送完外卖后,他们就要回到面包店门口等待,如果此时没有订单,他们就在店门口等着,直到有订单或者店主解雇。但是,由于生意比较好,订单多,但数量还未超过预期的数量,4个长期雇佣的外卖小哥都出去送餐了,新来的订单无法解决,于是店主决定,再雇佣一些临时的外卖小哥,但一共雇佣的外卖小哥的总人数不能超过十,这就意味着临时雇佣的外卖小哥的人数,只能有6个。当临时雇佣的外卖小哥送完订单后,他们也会回到面包店等待新的订单,但不同于长期雇佣的外卖小哥,临时雇佣的外卖小哥在面包店门口等的时间不是太长,如果在这段时间里没有订单,那么外卖小哥就被解雇了,就可以去其他店送餐了。后来,生意越来越好,订单太多了,10个外卖小哥都出去送餐了,店主就采取一些措施来拒绝一些订单。
这个例子中:
长期雇佣的外卖小哥就是核心线程
临时雇佣的外卖下个就是临时线程
订单就是任务
订单太多就是阻塞队列满了
店主采取的措施就是拒绝策略
拒绝策略
java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy
在调用者的线程中执行任务,除非执行程序已关闭,否则该任务将被丢弃。
就是:谁调用谁执行
java.util.concurrent.ThreadPoolExecutor.AbortPolicy
抛出异常
java.util.concurrent.ThreadPoolExecutor.DiscardPolicy
把任务直接丢弃,丢弃就相当于以后再也不会执行这个任务了
java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy
中断一个执行任务时间最长的线程,将当前任务执行
十、原子类
原子类的变量是线程安全的,多个线程进行就该也不会出现不安全的情况
java.util.concurrent.atomic
AtomicBoolean
原子性boolean类型变量
AtomicInteger
原子性int类型变量
AtomicIntegerArray
原子性的int类型数组
AtomicLong
原子性long类型变量
AtomicLongArray
原子性的long类型数组
使用方法:以int类型举例
public class Main {
private static java.util.concurrent.atomic.AtomicInteger integer = new java.util.concurrent.atomic.AtomicInteger(2);
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
integer.getAndIncrement();
}
}
};
Thread thread1 = new Thread(runnable, "线程1");
Thread thread2 = new Thread(runnable, "线程2");
thread1.start();
thread2.start();
while (Thread.activeCount() > 1)
Thread.yield();
System.out.println(integer);
}
}
结果
200002
这句代码
private static java.util.concurrent.atomic.AtomicInteger integer = new java.util.concurrent.atomic.AtomicInteger(2);
创建了一个原子性的int类型变量,初始值为2
当然,你不写初始值,值就是0
原子变量的操作
以int类型的说明
int getAndDecrement()
使值减少1
int getAndIncrement()
使值增加1
int getAndSet(int newValue)
设置新的值,方法返回值为旧值
int getOpaque()
返回当前的值
int get()
返回当前的值
这两种方法作用一样,但具体的实现不一样
原子变量值增加或减少的原理
CAS + 自旋(循环)
CAS
Compare and Swap
假设内存地址中存放的值为V,旧的预期值为O,新的值为N
原理:
某个线程要修改主存中x的值,当前x的值为V
这时该线程的操作
1.获取主存中x的值V,此时令O预期值等于V
2.修改x的值为N
3.把值N写回主存
在执行第3步时,就会进行判断:是否O与V相等
相等:线程将N值写入主存,之后主存中x的值为N
不相等:重新执行1、2、3步操作,到第三步操作还会进行判断,相等就写入主存,不相等就重新执行…
CAS中的 A->B->A 问题
当线程在执行1~3步的时间内,主存中x的值由原来的V变成了N又变成了V,对于该线程来说,察觉不到主存中的值变化了,所以就出现问题了
举个例子,这里我加入时间的概念,用来体现出线程1修改值的速度比线程2修改值的速度慢,衬托值由 V->N->V ,当然实际线程执行速度非常快,这里大家不必纠结
从上面的例子中可以看到
主存中 x 的值由 V->N->V 但线程1是不知道的
解决方法:
添加一个版本号,当x的值被修改后,就让版本号+1
添加版本号后正常的流程为
线程
1.读取主存中x的值V和版本号1,则预期的旧值O=V,预期的版本号为1
2.修改x的值为N
3. 读取主存中x的值V,判断是否与O相等
4.判断后发现相等,读取主存中x的版本号,判断是否相等
5.判断后发现相等,将主存的值修改为N,版本号+1
上述流程只是大概,具体请看源码查看
解决ABA问题流程
在Java5之后的版本中,出现的java.util.concurrent.atomic.AtomicStampedReference 类来解决ABA问题
自旋
我个人的理解为:
当V不等于O时,循环执行前面的操作,直到V与N相等的过程,叫做自旋
说白了,自旋就循环
缺点:循环操作会占用系统资源,因为是在循环操作执行代码
使用场景:
1.同一个时间点,只用单个线程对共享变量进行操作
2.CAS的执行时间不能太长,否则线程冲突几率大(适用于自己实现类似CAS功能的场景)
十一、线程安全的优化
锁粗化
多次加锁的代码连续写在一起,JDK在编译成字节码文件时,会合并成加锁一次
锁消除
对局部变量调用synchronized修饰的方法时,编译成字节码文件时,不会加锁
十二、java.util.concurrent包里的其他“锁”
java.util.concurrent.Semaphore
java.util.concurrent.CountDownLatch
java.util.concurrent.CyclicBarrier
十三、java.util.concurrent.ConcurrentHashMap
线程安全的HashMap
1.底层数据结构
JDK8 后:数组+链表+红黑树
2.实现 读读并发 读写并发 写写互斥
键值对中
键K 为 final 修饰
值V 为 volatile 修饰
3.加锁细粒度化
4.迭代器为安全失败迭代器
一个地方修改,另一个地方还可以遍历