JUC指的是全称叫java.util.concurrent包下的工具类。用中文概括一下,JUC的意思就是java并发编程工具包。其下包含三个最常用的并发编程的工具类:java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks。
其中并发的三大特性:原子性,可见性,有序性。
public class JUC {
public static void main(String[] args) throws InterruptedException {
// 并发:多线程操作同一个资源类,
//传统模式通过使一个类继承或实现Runnable接口变成纯粹的一个线程类,然后在main方法里通过new Thread(new 自定义线程类).start()开启线程
//这样就会造成耦合度较高,不符合Java的OOP思想,为了解耦,通过配合使用jdk8新特性lambda表达式实现新的线程启动方法
//使得类纯粹就是自己而不是一个线程类
Ticket ticket = new Ticket();
// @FunctionalInterface 函数式接口
new Thread(() -> {
for (int i = 1; i < 20; i++) {
//把资源类丢入线程
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "C").start();
}
}
class Ticket {
//假设车站还有40张票
private int number = 40;
// 卖票的方式---加synchronized锁保证线程的安全
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了" + (number-(--number) + "票,剩余:" + number));
}
}
}
部分结果演示:
A卖出了1票,剩余:24
A卖出了1票,剩余:23
B卖出了1票,剩余:22
B卖出了1票,剩余:21
B卖出了1票,剩余:20
B卖出了1票,剩余:19
B卖出了1票,剩余:18
B卖出了1票,剩余:17
B卖出了1票,剩余:16
B卖出了1票,剩余:15
B卖出了1票,剩余:14
B卖出了1票,剩余:13
B卖出了1票,剩余:12
B卖出了1票,剩余:11
B卖出了1票,剩余:10
B卖出了1票,剩余:9
B卖出了1票,剩余:8
B卖出了1票,剩余:7
B卖出了1票,剩余:6
B卖出了1票,剩余:5
B卖出了1票,剩余:4
C卖出了1票,剩余:3
C卖出了1票,剩余:2
C卖出了1票,剩余:1
C卖出了1票,剩余:0
Process finished with exit code 0
我们通过jdk官方文档可以看到,实现lock锁的实现类有三个:
我们在这里演示用的是ReentrantLock重入锁,通过点击源码进去查看,我们可以发现,使用重入锁默认用的是非公平的形式,如下图所示:
公平锁的意思简单来说就是十分公平,可以先来后到;而非公平锁的意思就是允许有插队的情况存在;
下面演示使用Lock锁实现火车站卖票的例子:
public class lock {
public static void main(String[] args) throws InterruptedException {
Ticket2 ticket2 = new Ticket2();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket2.sale();
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket2.sale();
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket2.sale();
}
}, "C").start();
}
}
class Ticket2 {
private int number = 40;
//1.new出新的lock锁,实现类为重入锁,可以在重入锁的参数添加true或false改变锁的公平性
Lock lock = new ReentrantLock();
public void sale() {
//2.加锁。一般在try方法前加锁
lock.lock();
try {
//3.编写业务代码
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了" + (number-(--number) + "票,剩余:" + number));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//4.解锁
lock.unlock();
}
}
}
Lock | synchronized |
---|---|
一个java类 | 内置的java关键字 |
可以判断锁的状态,是否获得锁 | 无法判断锁的状态 |
手动开关锁 ,不释放锁会造成死锁 | 自动加锁释放锁 |
不一定会等待下去,通过tryLock尝试获取锁,等不到就结束 | 线程一获得锁后假如阻塞了,线程二会一直等待 |
可重入锁,不可中断,非公平锁 | 可重入锁,可中断,可改变公平性 |
适合锁少量的代码同步问题 | 适合锁大量的同步代码 |
public class ProducerAndConsumer {
public static void main(String[] args) {
Data data = new Data();
//编写两个线程类
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
}
}
class Data {
//数字资源类
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
//如果number不等于0,等待消费,停止生产
while (number != 0) {
//使用if循环可能会造成虚假唤醒的情况
//业务代码
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
//如果number等于0,停止消费,等待生产
while (number == 0) {
//业务代码
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
this.notifyAll();
}
}
执行结果就不在这里演示了。但是要提的一个问题是,假如我现在不止两个线程才运行,我多两个线程运行,运行结果会是怎么样的呢?
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
运行结果:
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
D=>1
C=>2
D=>3
C=>4
D=>5
C=>6
D=>7
C=>8
D=>9
C=>10
D=>11
C=>12
D=>13
C=>14
D=>15
C=>16
D=>17
C=>18
D=>19
C=>20
那为什么会这样呢?这里就涉及到一个问题:虚拟唤醒的问题。这是因为在循环出我们用的是if循环,而if循环所带来的直接影响就是当wait被唤醒后直接从this.wait(),后面的语句继续执行,不是重新执行本方法,简单来说就是if循环只会判断一次,而while循环会不断的判断当时的情况,因而造成出现上述情况。我们来看看官方文档的描述:
因此,我们只需要将上述代码if循环改成while循环就好啦
使用synchronized锁的时候我们通过wait方法让线程等待,通过notifyAll的方法唤醒线程,那么我们使用Lock锁的时候应该用什么让线程等待,又是用什么让线程唤呢?
这里我们使用的是condition的方法去替代wait,notifyAll,他们的本质意思是一样的。
public class ConditionLock {
public static void main(String[] args) {
Data2 data = new Data2();
//编写两个线程类
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
class Data2 {
//数字资源类
/**
* 使用synchronized锁的时候我们通过wait方法让线程等待,通过notifyAll的方法唤醒线程
* 那么我们使用Lock锁的时候应该用什么让线程等待,又是用什么让线程唤醒呢?
* 这里我们使用的是condition的方法去替代wait,notifyAll,他们的本质意思是一样的
* */
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//+1
public void increment() throws InterruptedException {
lock.lock();//加锁
try {
while (number != 0) {
//业务代码
condition.await();//替代synchronized的wait
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
condition.signalAll();//唤醒所有,替代synchronized的notifyAll
}catch (Exception e){
e.printStackTrace();
}
finally {
lock.unlock(); //释放锁
}
}
//-1
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
//业务代码
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
既然synchronized实现的方法和我们用lock锁实现的效果基本都是一致的,并且方法都几乎是相同的,无非就是使用lock的时候我们通过condition来实现唤醒和等待,那么使用lock的优势又是在哪里呢?
很显然,我们如果运行上面的代码会发现,它的唤醒是无序的,即他没有一个特定的顺序,假如说我们想让A执行完之后执行B,B执行完之后只从C,即:A->B->C->A这样的一个顺序,我们这时候就可以通过condition精确唤醒。
演示代码如下:
public class ConditionLock2 {
public static void main(String[] args) {
Data3 data = new Data3();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.printA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.printB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.printC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
}
}
class Data3 {
private int number = 1;
private Lock lock = new ReentrantLock();
//新建三个监视器来分别监视唤醒不同的线程
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
//A
public void printA() throws InterruptedException {
lock.lock();
try {
//业务代码
while (number != 1) {
//如果不是1都等待,只有当为1的时候输出AAAA
condition1.await();
}
number = 2;//让condition2唤醒
System.out.println(Thread.currentThread().getName() + "=>AAAAAAAAAAA");
//通知2线程,注意这里不是signalAll
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//B
public void printB() throws InterruptedException {
lock.lock();
try {
//业务代码
while (number != 2) {
condition2.await();
}
number = 3;
System.out.println(Thread.currentThread().getName() + "=>BBBBBBBBBBB");
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//C
public synchronized void printC() throws InterruptedException {
lock.lock();
try {
//业务代码
while (number != 3) {
condition3.await();
}
number = 1;
System.out.println(Thread.currentThread().getName() + "=>CCCCCCCCCC");
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
八种锁的现象深入浅出的理解锁
代码片段一:我们为Phone类的send,call方法加锁,然后先调用发短信功能,然后休眠一秒,再调用打电话功能,那么,实现打电话呢还是先发短信呢?
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.send();
}, "A").start();
//休眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone {
//发短信
public synchronized void send() {
System.out.println(Thread.currentThread().getName()+"发短信");
}
//打电话
public synchronized void call() {
System.out.println(Thread.currentThread().getName()+"打电话");
}
}
以上代码执行的时候输出的结果是什么呢?
A发短信
B打电话
结果无论怎么运行,一定是先发短信再打电话,为什么呢?那如果我们让发短信的方法休眠4秒呢?
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.send();
}, "A").start();
//休眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone {
public synchronized void send() {
//休眠四秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"发短信");
}
public synchronized void call() {
System.out.println(Thread.currentThread().getName()+"打电话");
}
}
结果无论如何运行,一定是发短信先,原因就在于synchronized
锁的是方法的调用者,我们的调用者只有一个,是phone,也就是说,无论是send方法还是call方法,他们用的都是同一个锁,都是phone对象的锁,因此,谁先拿到锁,谁就先使用。
代码片段二:我们再phone类新增了一个普通方法hello,然后再执行这段代码,这次让b输出hello方法,其他不变,那么结果是什么呢?
public class Test2 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(() -> {
phone.send();
}, "A").start();
//休眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.hello();
}, "B").start();
}
}
class Phone2 {
public synchronized void send() {
//休眠四秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"发短信");
}
public synchronized void call() {
System.out.println(Thread.currentThread().getName()+"打电话");
}
//新增一个普通方法
public void hello(){
System.out.println("hello!");
}
}
结果:
这次是hello先输出。因为新增的方法是普通方法,没有锁,不是同步方法,不受锁的影响,因此线程B并不会等待A释放锁再去执行。
hello!
A发短信
那如果我新new出一个对象phone,现在有两个对象,两个同步方法,一个发短信,一个打电话,那么是谁先执行呢?
代码如下:
public class Test2 {
public static void main(String[] args) {
//现在这里有两个对象,两把锁,两个调用者
Phone2 phone = new Phone2();
//新new出一个对象
Phone2 phone2 = new Phone2();
new Thread(() -> {
phone.send();
}, "A").start();
//休眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.call();
}, "B").start();
}
}
class Phone2 {
public synchronized void send() {
//休眠四秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"发短信");
}
public synchronized void call() {
System.out.println(Thread.currentThread().getName()+"打电话");
}
public void hello(){
System.out.println("hello!");
}
}
结果如下:
因为这次new了两个对象,两个同步方法,这两个对象各自拥有各自的锁,互不影响,因此休眠时间短的先输出,因此B先打电话,过了四秒之后,A再发短信。
B打电话
A发短信
代码片段三:
如果我们这次给phone类的两个方法加上static变为静态方法呢?输出的结果会是什么呢?
public class test3 {
public static void main(String[] args) {
Phone3 phone = new Phone3();
new Thread(() -> {
phone.send();
}, "A").start();
//休眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone3 {
//添加static变为静态方法
public static synchronized void send() {
//休眠四秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"发短信");
}
public static synchronized void call() {
System.out.println(Thread.currentThread().getName()+"打电话");
}
}
输出结果:
A发短信
B打电话
那如果我这时候,又new了一个对象呢,两个对象,两个同步代码块呢?输出结果是什么呢?
public class test3 {
public static void main(String[] args) {
Phone3 phone = new Phone3();
//new多一个新的对象
Phone3 phone3 = new Phone3();
new Thread(() -> {
phone.send();
}, "A").start();
//休眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone3.call();
}, "B").start();
}
}
class Phone3 {
public static synchronized void send() {
//休眠四秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"发短信");
}
public static synchronized void call() {
System.out.println(Thread.currentThread().getName()+"打电话");
}
}
输出结果:
A发短信
B打电话
原因是因为通过static修饰的方法它是属于静态方法,它是属于这一个class类,我们知道静态方法可以直接调用而不需要new出一个新的实例对象去调用。这里得意思就有点像这个,因为用的是static修饰的静态方法,因此synchronized 锁的是这个class类本身,而不是这个方法的调用者,因此无论你new多少个实例出来,class类只有一个,他还是只有一把锁,锁的是这个class类,因此,谁先拿到锁,就谁先输出结果。
代码片段四:这次如果我们一个方法是普通同步方法,一个方法是静态同步方法,只有一个对象,那么我们先输出打电话呢,还是先输出发短信呢?
public class test4 {
public static void main(String[] args) {
Phone4 phone = new Phone4();
new Thread(() -> {
phone.send();
}, "A").start();
//休眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone4 {
//静态同步方法
public static synchronized void send() {
//休眠四秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"发短信");
}
//普通同步方法
public synchronized void call() {
System.out.println(Thread.currentThread().getName()+"打电话");
}
}
结果如下:
B打电话
A发短信
那如果我现在有两个对象,一个对象调用的是静态同步方法,一个调用的是普通同步方法呢?
public class test4 {
public static void main(String[] args) {
Phone4 phone = new Phone4();
Phone4 phone4 = new Phone4();
new Thread(() -> {
phone.send();
}, "A").start();
//休眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone4.call();
}, "B").start();
}
}
class Phone4 {
//静态同步方法
public static synchronized void send() {
//休眠四秒
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"发短信");
}
//普通同步方法
public synchronized void call() {
System.out.println(Thread.currentThread().getName()+"打电话");
}
}
结果:
B打电话
A发短信
这是因为同步方法锁的是调用者,静态同步方法锁的是这个class,这是两个锁,因此谁的时间段谁先执行。
多个线程操作同一个ArrayList集合的时候会有不安全的现象,因此ArrayList是线程不安全的。
public class unsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
报的异常:
java.util.ConcurrentModificationException并发修改异常
解决方案有三种,最常用的是使用CopyOnWriteArrayList:
public class unsafeList {
public static void main(String[] args) {
//解决方案1.使用vector集合。底层其实用的还是synchronized锁,用这个锁效率就会低
//List list =
//解决方案2.使用collections的工具类,将不安全的集合ArrayList转换成安全的synchronizedList即可
//List list = Collections.synchronizedList(new ArrayList());
//解决方法3.使用JUC包下的CopyOnWriteArrayList。最常用的方法。
//CopyOnWrite 写入时复制,写入完之后再插入。避免写入覆盖
//CopyOnWrite相比于vector方法
List<String> list = new CopyOnWriteArrayList<>();
//List list = new ArrayList();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
Set的不安全例子和List大同小异,直接放代码:
public class unsafeSet {
//解决方案一,通过collections工具类. Set set = Collections.synchronizedSet(new HashSet());
//解决方法二. 通过 Set set = new CopyOnWriteArraySet();
public static void main(String[] args) {
Set<String> set = new CopyOnWriteArraySet<String>();
for(int i = 0; i < 1000; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
同上
public class unsafeMap {
//解决方案一,通过collections工具类. Map map = Collections.synchronizedMap(new HashMap());
//解决方法二. 通过 Set set = new CopyOnWriteArraySet();
public static void main(String[] args) {
Map<String,String> map = new ConcurrentHashMap<String, String>();
for(int i = 0; i < 1000; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
Callable: 返回结果并且可能抛出异常的任务。
优点:
可以获得任务执行返回值。
通过与Future的结合,可以实现利用Future来跟踪异步计算的结果。
Callable可以抛出异常。
public class CallableTest01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread thread = new MyThread();
//适配类,接收实现Callable的线程类的实例对象
FutureTask<String> FutureTask = new FutureTask<>(thread);
//通过Thread开启线程
new Thread(FutureTask,"A").start();
//new一个新的线程,因为有缓存的存在,只会打印一个语句。用于提高效率
new Thread(FutureTask,"B").start();
//通过调用适配类的get方法,FutureTask.get()获取到返回值,但是可能存在阻塞现象
//因为在Call方法内的业务逻辑代码可能会是耗时的操作,就会造成一直等待的现象,所以一般把他放在最后一行
System.out.println(FutureTask.get());
}
}
//传递的参数类型,就是我们需要返回时的参数类型
class MyThread implements Callable<String> {
public String call() {
System.out.println("成功调用call方法");
return "1024";
}
}
可以理解为减法计数器,主要有两个方法:
countDownLatch.countDown();//数量-1
countDownLatch.await();//等待计数器归0再往下执行
一般常用于一些必须要完成的线程任务
代码演示:
public class CountDownLatchTest01 {
public static void main(String[] args) throws InterruptedException {
//执行任务的数量
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+" Go out");
countDownLatch.countDown();//数量-1
}, String.valueOf(i)).start();
}
//计数器不归0,不会执行以下操作
countDownLatch.await();
System.out.println("Close door");
}
}
有减法计数器,就会有加法计数器,这里的CyclicBarrier就相当于加法计数器,只有当所有线程都执行完操作才可以进行下一步
public class CyclicBarrierTest01 {
public static void main(String[] args) {
//只有7个线程都完成才可以执行召唤神龙的操作
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("集齐了七龙珠召唤神龙!");
});
for (int i = 1; i <= 7; i++) {
int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"拿到了第了"+temp+"颗龙珠");
try {
cyclicBarrier.await();//没有完成就继续等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
信号量,相当于一个通行证。
主要方法:
semaphore.acquire();获取,假设满了,等待,直到释放
semaphore.release();释放,当前信号量+1,唤醒等待线程
主要用于共享资源互斥使用,并发限流,控制最大线程数
public class SemaphoreTest01 {
public static void main(String[] args) {
//信号量,在这里可以理解为通信证,没有通行证不能占用资源:理解为停车位
Semaphore semaphore = new Semaphore(3);
//假设有6个线程6台车,但只有3个停车位
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
//获取
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位");
//停车3秒
TimeUnit.SECONDS.sleep(3);
System.out.println("三秒钟后" + Thread.currentThread().getName() + "离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
使用方法:
通过ReentrantReadWriteLock实例生成对象,直接调用即可。如:如果想调用写锁:
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.writeLock().lock()
使用方法上和lock没什么区别。
public class ReadWriteLock {
public static void main(String[] args) throws InterruptedException {
myCache2 myCache = new myCache2();
//1-5个线程分别只进行写的操作,写的时候只有一个线程写
for (int i = 1; i <= 5; i++) {
int temp = i;
new Thread(() -> {
myCache.put(temp + "", temp + "");
}, String.valueOf(i)).start();
}
TimeUnit.SECONDS.sleep(3);
//1-5个线程分别只进行读的操作,读的时候可以有多个线程读
for (int i = 0; i <= 5; i++) {
int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
/*
* 自定义缓存--加锁
* */
class myCache2{
public volatile Map<String,Object> map = new HashMap<>();
final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
//存,写
public void put(String key,Object value){
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入成功");
}catch (Exception e){
e.printStackTrace();
}
finally {
lock.writeLock().unlock();
}
}
//取,读
public void get(String key){
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"取出"+key);
map.get(key);
System.out.println(Thread.currentThread().getName()+"取出成功");
}catch (Exception e){
e.printStackTrace();
}
finally {
lock.readLock().unlock();
}
}
}
/*
* 自定义缓存--不加锁
* */
class myCache{
public volatile Map<String,Object> map = new HashMap<>();
//存,写
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入成功");
}
//取,读
public void get(String key){
System.out.println(Thread.currentThread().getName()+"取出"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"取出成功");
}
}
阻塞队列可以分开理解为阻塞和队列。在写入时候如果队列满了,则阻塞等待。在读取时如果队列是空的,则阻塞等待生产。
常用于多线程并发处理、线程池!
操作 | 抛出异常 | 有返回值不抛出异常 | 阻塞等待 | 超时等待,有返回值 |
---|---|---|---|---|
添加 | add;异常种类:java.lang.IllegalStateException: Queue full | offer | put | offer(添加的内容,睡眠时间, 睡眠种类) |
删除 | remove;异常种类: java.util.NoSuchElementException | poll | take | poll(添加的内容,睡眠时间, 睡眠种类) |
获取队首 | element;异常种类:java.util.NoSuchElementException | peek | - | - |
第一种:抛出异常的情况
public class fourApi {
public static void main(String[] args) {
test1();
}
//测试普通的抛出异常的方式
public static void test1(){
//参数表示queue队列的长度
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);
//添加元素
queue.add("a");
queue.add("b");
queue.add("c");
//演示多一个增加异常情况
//queue.add("d");
System.out.println("===================");
//打印队首
System.out.println("队首元素是"+queue.element());
System.out.println("队列的长度是"+queue.size());
System.out.println("===================");
//取出元素
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
//演示多一个删除异常情况
//System.out.println(queue.remove());
// Iterator iterator = queue.iterator();
// while (iterator.hasNext()){
// System.out.println(iterator.next());
// }
}
}
第二种:有返回值不抛出异常的情况
public static void test2(){
//参数表示queue队列的长度
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);
//添加元素
queue.offer("a");
queue.offer("b");
queue.offer("c");
//演示多一个增加异常情况
//queue.offer("d");
System.out.println("===================");
//打印队首
System.out.println("队首元素是"+queue.peek());
System.out.println("队列的长度是"+queue.size());
System.out.println("===================");
//取出元素
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
//演示多一个删除异常情况
System.out.println(queue.poll());
}
第三种:阻塞等待
public static void test3() throws InterruptedException {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
queue.put("a");
queue.put("b");
queue.put("c");
//一直阻塞,进入等待不会停止
//queue.put("d");
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
//一直阻塞,进入等待不会停止
//queue.take();
}
第四种:超时等待
public static void test4() throws InterruptedException {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.offer("a",2, TimeUnit.SECONDS));
System.out.println(queue.offer("b",2, TimeUnit.SECONDS));
System.out.println(queue.offer("c",2, TimeUnit.SECONDS));
//有返回值,返回值是布尔类型,如果等待超时则推出等待
System.out.println(queue.offer("d",2, TimeUnit.SECONDS));
System.out.println(queue.poll(2, TimeUnit.SECONDS));
System.out.println(queue.poll(2, TimeUnit.SECONDS));
System.out.println(queue.poll(2, TimeUnit.SECONDS));
//
System.out.println(queue.poll(2, TimeUnit.SECONDS));
}
同步队列是一种相对来说较为特殊的队列,他自己本身不存储值,它的容量为1。存入之后必须等待取出才能进行下一次存入。
public class TestSynchronousQueue {
public static void main(String[] args) {
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println("放入一个元素a");
synchronousQueue.put("a");
System.out.println("放入一个元素b");
synchronousQueue.put("b");
System.out.println("放入一个元素c");
synchronousQueue.put("c");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("移除一个元素");
System.out.println(synchronousQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println("移除一个元素");
System.out.println(synchronousQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println("移除一个元素");
System.out.println(synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
1、什么是池化技术?
答:简单理解就是事先准备一部分资源,使用就拿走,用完就还给池。
2、使用线程池的好处?
答:线程池其实就相当于我们缓冲区的概念,线程池中会先启动若干数量的线程,这些线程都处于睡眠状态。当客户端有一个新的请求时,就会唤醒线程池中的某一个睡眠的线程,让它来处理客户端的这个请求,当处理完这个请求之后,线程又处于睡眠的状态。
1)这样最大的好处就是避免了繁琐的创建线程销毁线程,降低了资源的损耗。
2)通过预备好的线程去执行服务,提高了响应速度。
3)方便对线程进行统一的管理。
4)实现了线程的复用,可用于控制并发数,更好的管理线程。
SingleThreadExecutor
FixedThreadPool
CachedThreadPool
public class Executor {
public static void main(String[] args) {
//通过Executors创建只有单个线程的线程池
//ExecutorService ThreadPool = Executors.newSingleThreadExecutor();
//通过Executors创建只有固个线程的线程池
ExecutorService ThreadPool = Executors.newFixedThreadPool(5);
//通过Executors创建可伸缩的动态线程池
//ExecutorService ThreadPool = Executors.newCachedThreadPool();
try {
for (int i = 1; i <= 100; i++) {
int temp = i;
//开启线程,通过调用execute方法
ThreadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了"+temp+"线程");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
//线程使用完之后一定要关闭线程
ThreadPool.shutdown();
}
}
}
无论是SingleThreadExecutor,还是FixedThreadPool,或者是CachedThreadPool,我们通过点进去它的源码可以查看到,它的底层本质上都是使用了一个叫ThreadPoolExecutor的方法。
public ThreadPoolExecutor(int corePoolSize,//核心线程数
int maximumPoolSize,//最大线程数
long keepAliveTime,//线程存活时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//线程池工厂
//拒绝策略
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = (System.getSecurityManager() == null)
? null
: AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
阿里巴巴开发者手册里面明确规定:
因此,在日后的开发中,我们不会使用上面的三大方法来创建线程池,而是通过最原始的方法ThreadPoolExecutor方法来创建线程池。在通过使用ThreadPoolExecutor来创建线程池例子演示的时候,先通过一幅图来更好的了解七大参数。
maximumPoolSize–最大线程数:我们假设有一个银行,这个银行对应的就是我们的线程池,这一个个柜台就相当于我们的线程,由图我们不难看到,maximumPoolSize最大线程数就是5,因为有5个柜台。
corePoolSize–核心线程数:一般我们去银行办理业务的时候,如果人不是很多,一般不是每个柜台都开启服务的,一般只开几个柜台,在我们下图这里。红色的柜台表示暂不服务,白色的柜台表示服务。不管怎么样,不管人多人少,一定有两个柜台在服务,这两个一定服务的柜台(线程)就叫核心线程。
BlockingQueue workQueue–阻塞队列:在这里就可以理解为候客区
keepAliveTime–线程存活时间:假设有一天两个柜台(线程在服务),候客区的人 (请求)已经满了,这个时候还有人想进来等待被服务,那么这个时候平时不服务的柜台(红色标注)会打开来进行服务
当服务完之后,人已经不多了,也不需要这么多柜台进行服务了,在经过一段等待时间后还是没有人来办理业务,那么这些柜台又会重新关闭,这段等待的时间就叫最大等待时间。
TimeUnit unit–单位:最大等待时间的单位
ThreadFactory threadFactory–线程池工厂:可以理解为建造银行(线程池)的建筑商
RejectedExecutionHandler–拒绝策略:可以理解为,当银行里面所有柜台都在服务了,候客区也满了,这个时候还想有人来办理业务,明显这个时候是处理不了的,太繁忙了,银行在这个时间段里告诉你先别服务,这就叫拒绝策略。
拒绝策略有四种:
1)多出来的线程,直接抛出异常
new ThreadPoolExecutor.AbortPolicy()
2)谁开启的这个线程,就让这个线程返回给谁执行。比如main线程开启的,那就返回给main线程执行
new ThreadPoolExecutor.CallerRunsPolicy()
3)如果队列线程数量满了以后,直接丢弃,不抛出异常
new ThreadPoolExecutor.DiscardPolicy()
>4)队列满了以后,尝试去和最早的线程竞争,也不会抛出异常
new ThreadPoolExecutor.DiscardOldestPolicy
下面展示自定义线程池:
public class MyPool {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 1; i <= 2; i++) {
int temp = i;
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了"+temp+"线程");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPoolExecutor.shutdown();
}
}
}
可以看到,当阻塞队列,即候客区没满的时候,只有两个核心线程在执行。
当候客区满了,核心线程忙不过来了的时候,3号服务窗口开始服务。
当我们的要求服务的人达到了9个,可以看到,我们使用了AbortPolicy抛出了异常
,具体异常为:
java.util.concurrent.RejectedExecutionException
1、CPU密集型:根据电脑运行的核数去定义最大线程数,通过
Runtime.getRuntime().availableProcessors()
获得本机电脑CPU核数。
2、IO密集型:根据程序中消耗IO的线程数,乘于2就是我们定义的最大线程数
//电脑本机CPU最大核数
System.out.println(Runtime.getRuntime().availableProcessors());
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors(),//电脑本机CPU最大核数
3,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
关于IO密集型与CPU密集型的描述–转载自Java技术栈
//传入类型T和返回类型R
public interface Function<T, R> {
R apply(T t);
}
public class hanshu {
public static void main(String[] args) {
// lambda表达式 //传入的参数
// Function function = (str)->{
//返回的字符串
// return str;
// };
// System.out.println(function.apply("asd"));
//匿名内部类方式的函数式接口,Function接口体的两个参数表示传入的参数和返回的参数类型
Function function = new Function<String,String>() {
@Override
public String apply(String o) {
return o;
}
};
System.out.println(function.apply("test"));
}
}
@FunctionalInterface
//传入类型T,返回布尔类型
public interface Predicate<T> {
boolean test(T t);
}
public class duanding {
public static void main(String[] args) {
//使用lambda表达式进行简化书写
Predicate<String> predicate = (s)->{
return s.isEmpty();};
System.out.println(predicate.test(""));
}
}
@FunctionalInterface
//只有输入没有返回值
public interface Consumer<T> {
void accept(T t);
}
public class consmuer {
public static void main(String[] args) {
Consumer<String> consumer = (s)->{
System.out.println(s);
};
consumer.accept("aaa");
}
}
@FunctionalInterface
//没有参数,只有返回值
public interface Supplier<T> {
T get();
}
public class gongji {
public static void main(String[] args) {
Supplier<String> supplier = ()->{
return "1024";
};
System.out.println(supplier.get());
}
}
来道题目感受一下:
题目要求:使用Stream进行筛选计算
输出的id必须为偶数
年龄必须大于23岁
用户名转为大写字母
用户名字字母倒着排序
只能输出一个用户
public class stream {
public static void main(String[] args) {
User user1 = new User(1, "a", 21);
User user2 = new User(2, "b", 22);
User user3 = new User(3, "c", 23);
User user4 = new User(4, "d", 24);
User user5 = new User(6, "e", 25);
//使用List集合,存储五个用户
List<User> list = new ArrayList<User>();
list.add(user1);
list.add(user2);
list.add(user3);
list.add(user4);
list.add(user5);
//使用Stream流进行计算
list.stream()
//filter返回由与此给定参数匹配的此流的元素组成的流,即返回对象。
.filter((u) -> {
return u.getId() % 2 == 0; })
.filter((u) -> {
return u.getAge() > 23;})
//map返回由给定函数应用于此流的元素的结果组成的流,即返回函数处理后返回地值。
.map((u -> {
return u.getName().toUpperCase();}))
//将结果逆序排列
.sorted((u1, u2) -> {
return u2.compareTo(u1);})
//System.out::println打印的是传入的参数,也就是Customer的参数泛型T,
// 这个T就是从list的流对象获取的T,最终都是List对象创建时指定的泛型,即User
.forEach(System.out::println);
}
}
fork有叉子的意思,叉子大家都见过,一个主体分成两个尖尖的“牙”,join有加入得意思。从字面上意思就很好理解,先分开再合并。因此,forkJoin的思想就一目了然,它的主要作用就是把一个大任务分割成若干个小任务,然后再把每个小任务得到的结果汇总得到大任务的结果。用一张图表示就是:
特点:工作窃取算法:
即当一个大任务被拆分成小任务的时候,有的线程可能提前完成了任务,此时闲着就会去帮助其他没完成工作的线程。将别的线程的任务窃取过来完成。为了减少竞争,一般线程采取的是双端队列。但是当双端队列只有一个任务的时候,窃取就会造成资源竞争,消耗更多的资源。
代码演示ForkJoin:
1、继承自RecursiveTask<>,类型为重写compute方法的类型
2、通过fork拆分任务
3、通过join合并任务
//自定义的forkjoin类
//继承自RecursiveTask,泛型为compute方法的参数类型
public class ForkJoinDemo extends RecursiveTask<Long> {
//定义初始加法的值
private long start;
//定义最有一个加法的值
private long end;
//临界值,这个值的上下执行不同的方法
private long temp = 1000000;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
//继承类中的抽象方法,我们在里面编写我们的计算方法体
@Override
protected Long compute() {
//如果最后一个值与第一个值之间的差小于临界值,执行普通的方法
if ((end - start) < temp) {
long sum = 0L;
for (Long i = start; i <= end; i++) {
//求和运算
sum += i;
}
//方法的返回值需要与继承类的类型相同
return sum;
//否则使用forkjoin拆分任务求和
} else {
//定义中间值拆分成两个任务
long middle = (start + end) / 2;
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
//把一个任务拆分为两个小任务,并且将两个任务压入队列中
task1.fork();
task2.fork();
//返回计算以后的结果
return task1.join() + task2.join();
}
}
}
测试代码:
public class ForkJoinTest01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
test01();
test02();
test03();
}
//使用最基本的循环遍历计算
public static void test01() {
long start = System.currentTimeMillis();
long sum = 0;
for (long i = 0; i <= 10_0000_0000L; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("基本循环花费的时间是:" + (end - start) + "结果是" + sum);
}
//使用ForkJoin进行计算
public static void test02() throws ExecutionException, InterruptedException {
//起始时间
long start = System.currentTimeMillis();
//创建一个ForkJoinPool池,创建过程类似于线程池
ForkJoinPool forkJoinPool = new ForkJoinPool();
//调用自己定义的类
ForkJoinDemo task = new ForkJoinDemo(0L, 10_0000_0000L);
//将自己的任务,提交至ForkJoinPool池中
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("forkJoin花费的时间是:" + (end - start) + "结果是" + sum);
}
//使用stream分支流计算
public static void test03() {
long start = System.currentTimeMillis();
//parallel返回平行的等效流。 可能会返回自己,因为流已经是并行的,或者因为底层流状态被修改为并行。
//reduce,将区间里的数求和
long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("stream分支流花费的时间是:" + (end - start) + " 结果是" + sum);
}
}
补充LongStream中上述方法的用法:
@Test
public void rangedClosedTest() {
LongStream ls = LongStream.rangeClosed(2L, 5L);
long[] lsA = ls.toArray();
for (long l : lsA) {
System.out.println(l);
}
}
/**
* 输出结果为,方法是返回其实参数到末尾参数中间的所有的值
* 2
* 3
* 4
* 5
* */
public class future {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//没有返回值的runAsync异步回调,使用CompletableFuture.runAsync方法,通过lambda实现线程
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"==>OK");
}
});
//输出123表明异步线程并不会阻塞其他线程执行
System.out.println("123");
//阻塞获取结果,因为异步方法调用还没开始,就已经执行到get方法了,因此这里的get方法肯定是会阻塞的
completableFuture.get();
}
}
public class FutureTest01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//有返回值(t, u)的supplyAsync异步回调
//返回值是错误的信息,返回类型是integer类型
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
//添加一个异常语句
int i = 10/0;
System.out.println(Thread.currentThread().getName() + "==>OK");
return 1024;
});
//获取异步回调对象的返回值(t, u),whenComplete对应成功时候的返回值,否则就是失败的返回值
System.out.println(completableFuture.whenComplete((t, u) -> {
//t为正常执行的信息
System.out.println("t=>" + t);
//u为错误的信息
System.out.println("u=>" + u);
}).exceptionally((e) -> {
//输出错误信息:java.lang.ArithmeticException: / by zero
System.out.println(e.getMessage());
return 333;
}).get());
}
}
JMM(Java Memory Model)Java内存模型,是一种规范和约定,他并不是真实存在的。
由于每个线程运行的时候,JVM都会在运行时数据区为其创建一个线程私有的Java栈,用于存储线程的私有数据,包括局部变量,基本类型等,在这里我们把它称之为工作内存。而JMM中则规定所有变量都存储在主存中,而主存是共享内存的,所有线程都可以访问。因此,在JMM中线程如果要对变量进行操作,就必须从主存中拷贝变量到自己的工作空间,在自己的工作空间对变量进行操作完成后,再把变量写回主存,不能直接操作主存中的变量。下面用一幅图来展示这个过程:
JMM八大操作名称解释:
1:lock: 把主内存变量标识为一条线程独占,此时不允许其他线程对此变量进行读写。
2:unlock:解锁一个主内存变量。
3:read: 把一个主内存变量值读入到线程的工作内存,强调的是读入这个过程。
4:load: 把read到变量值保存到线程工作内存中作为变量副本,强调的是读入的值的保存过程。
5:use: 线程执行期间,把工作内存中的变量值传给字节码执行引擎。
6:assign(赋值):字节码执行引擎把运算结果传回工作内存,赋值给工作内存中的结果变量。
7:store: 把工作内存中的变量值传送到主内存,强调传送的过程。
8:write: 把store传送进来的变量值写入主内存的变量中,强调保存的过程。
1、线程解锁前,必须把共享变量立刻刷回主存
2、线程加锁前,必须读取主存中的最新值到工作内存中
3、加锁和解锁必须是同一把锁
4、不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
5、不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存6、不允许一个线程将没有assign的数据从工作内存同步回主内存
7、一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
8、一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
9、如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
10、 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
11、对一个变量进行unlock操作之前,必须把此变量同步回主内存
依旧用一张图解释
假设现在有两个线程A和B,线程B从主存拿到了变量的副本并将Flag的值修改成了False,写回了主存中,而此时线程A拿到的还是最初的Flag=True的副本(个人认为这里可以理解为脏读),这个时候就可能会英发一些不安全的操作。下面来一段代码演示:
/**
* 这里的main线程相当于上图的B线程,Thread1相当于线程A
*按照我们的理解主线程修改num=false了,而Thread1不知道num的值修改了,因为线程与线程之间
* 在此时是不可见的,Thread1并不知道Main线程已经修改了主存,因此Thread1里面的线程依然再循环,并没有停止
* */
public class JMMTest {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag) {
//while循环里如果打印循环会停止,暂时不知道为什么
}
},"ThreadA").start();
//确保让开启的其他线程先执行
TimeUnit.SECONDS.sleep(2);
//修改变量的值,观察线程的变化
flag = false;
System.out.println(flag);
}
}
那么,我们怎么让线程之间保证可见性呢?接下来引出我们的下一个小节Volatile。
首先,volatile是Java中的一个关键字。它是轻量级的Synchronized锁。它保证了变量的可见性,但是没有保证原子性,禁止指令重排(有序性)。如果一个变量被声明为volatile,那么Java线程内存模型所有线程看到的这个变量都是一致性的。
这里对上一小节遗留的问题进行代码修改。
public class JMMTest {
private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag) {
//while循环里如果打印循环会停止,暂时不知道为什么
//上网查询了下原因是因为system.out.println底层使用的是synchronized锁?可能是因为这个原因
}
},"ThreadA").start();
//确保让开启的其他线程先执行
TimeUnit.SECONDS.sleep(2);
//修改变量的值,观察线程的变化
flag = false;
System.out.println(flag);
}
}
运行之后可以明显地看到,Thread1不会再出现死循环的现象,这说明volatile保证了可见性。
原子性:不可被中断的一个或一系列操作。线程A在执行的时候不能被分割或被停止,要么同时成功,要么同时失败。
代码验证:
public class atomicTest {
//保证变量在各个线程可见
private static volatile int num = 0;
//加synchronized可以保证结果
private static void add() {
num++;
}
public static void main(String[] args) throws InterruptedException {
//开启10个线程,每一个线程中执行100次方法
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
add();
}
}).start();
}
//避免我们开启的线程还没有结束就执行主线程
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(num);
}
}
输出的结果正确的应该是1000,但是试了很多次结果都没有到1000,说明没有保证了原子性。num++并不是原子性操作。我们通过JClasslib插件可以看到:
num++这个操作其实它的底层字节码是不止一行的,是有可能被多个线程操作的,那么我们除了使用lock锁或者synchronized锁解决以外,还有什么办法呢?
我们可以使用java.util.concurrent.atomic 包下的AtomicInteger类。
改造后的方法:
public class atomicTest2 {
//使用java.util.concurrent.atomic包下的原子类
private static AtomicInteger num = new AtomicInteger();
private static void add() {
//使用原子类的增加方法,不是单纯的+1操作,用的是底层的CAS
//关于CAS在接下来会进行描述
num.getAndIncrement();
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int i1 = 0; i1 < 100; i1++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(num);
}
}
原子类的操作底层都和操作系统挂钩,在内存中修改值,关于这一部分的知识点会在接下来的CAS小节讲解复习。
我们得先明确,什么是指令重排:
处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的(前提是指令之间没有数据依赖性)。但是,保证结果一致是对于单线程而言的!在多线程操作下,就会有可能影响并发执行的正确性。如:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
/**
*上面的代码里面指令之间没有依赖性
*假设我先执行语句2,线程2条件满足跳出休眠
*线程2里面又要用到语句1的初始化参数,语句1没有初始化,这个时候就会报错
* */
五种单例模式测试
public class CAS01 {
//CAS:比较当前工作内存的值和主存中的值,如果值是期望的,那么执行操作,否则就一直循环
public static void main(String[] args) {
//AtomicInteger原子类,参数为初始值,不赋值则为0
AtomicInteger atomicInteger = new AtomicInteger();
//compareAndSet(int expect, int update),第一个参数是期望值,如果达到期望值,则更新为第二个参数
//CAS是CPU的并发原语!
System.out.println(atomicInteger.compareAndSet(0, 2021));//true符合预期更新为2021
//输出当前的值
System.out.println(atomicInteger.get());
atomicInteger.getAndIncrement();//相当于I++操作
System.out.println(atomicInteger.compareAndSet(2022, 2023));
System.out.println(atomicInteger.get());
}
}
底层源码窥探:
我们都知道Java无法直接操作本地计算机的内存,虽然说可以通过本地方法native调用C++操作内存。但是Java给自己留了一个后门,可以通过Unsafe类操作内存。上面的代码中,AtomicInteger的getAndIncrement方法中的value参数(内存地址偏移量)就是通过unsafe类的objectFieldOffset获取的,底层用了do,while自旋锁方式,通过weakCompareAndSet(CAS)进行操作。
在这里我们得先明确CAS的核心就是比较原来的值和期望值有没有变化,如果没有变化则更新,如果变化了就不更新,这一点我们可以从示例代码很清楚的看到。那么问题来了,CAS它只检测值是不是原来那个值,它并不知道这个值是否被改动过,就好比如下图:
线程A从主存复制了工作副本准备进行CAS(1,2)操作,这个时候A也是1,如果正常执行那么就会通过CAS把值变为2;但是在线程A执行之前,线程B抢先一步操作把主存A的值从1变为3然后再变为1,此时线程A从主存拷贝工作副本的时候依旧认为这个1是原来的1,没有发生改变,其实已经改变过了没有被发现而已,这就是ABA问题。
代码展示:
public class CAS02 {
public static void main(String[]args){
AtomicInteger atomicInteger = new AtomicInteger(0);
// ============== 捣乱的线程 ==================
System.out.println(atomicInteger.compareAndSet(0, 1));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(1, 0));
System.out.println(atomicInteger.get());
// ============== 期望的线程 ==================
System.out.println(atomicInteger.compareAndSet(0, 3));
System.out.println(atomicInteger.get());
}
}
那么我们如何解决这个问题呢?JDK1.5以后atomic包下提供了一个AtomicStampedReference类可以解决这个问题。通过这个类添加版本号,实现版本控制,每更改一次就更新版本号。本质上是乐观锁思想的一种体现
public class ABAReslove {
//初始值是1,初始版本号是1
//这里用的是Integer包装类,java为了提高效率缓存了-128~127之间的数字来实现复用,超过这个范围的会在堆上new出一个新的对象
//工作中主要是比较对象
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);
public static void main(String[] args) {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); //获取当前版本号
System.out.println("a1=>" + stamp);//输出当前版本号
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//期望值,更新值,当前的版本号,更新的版本号
atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
//打印最新版本信息
System.out.println("a2=>" + atomicStampedReference.getStamp());
//判断是否更新成功,返回布尔类型
System.out.println(atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
//打印最新版本信息
System.out.println("a3=>" + atomicStampedReference.getStamp());
}, "a").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
//线程b当前的版本号
System.out.println("b1=>" + stamp);
try {
//睡眠3秒确保线程A先执行完并更改
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 6,
stamp, stamp + 1));
//由于在线程B执行之前A已经更改过数值,版本号发生了改变,因此更新失败
System.out.println("b2=>" + atomicStampedReference.getStamp());
}, "b").start();
}
}
锁的一些相关补充