Java JUC
Java并发编程,在Java 5.0 提供了Java.util.concurrent,在此我们简称JUC。我们希望借此来提高性能,而不是拉低我们的性能。
1、volatile与内存可见性问题
先来看段线程代码
package com.kj.test;
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo demo = new ThreadDemo();
new Thread(demo).start();
while (true) {
if (demo.isFlag()) {
System.out.println("------");
break;
}
}
}
}
class ThreadDemo implements Runnable {
private boolean flag = false;
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag=" + flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
这段代码运行后,将只输出flag=true
,一直保持运行,不会停止
但是循环中判断一直为false
这里就涉及到内存可见性的问题
可见性的问题
JVM会为每一个线程提供独立的缓存用于提高效率
在程序开始执行时,因为分线程sleep了1000单位
主线程更快开始,主线程从主存中读取到数据flag为false
由于while(true)调用的是操作系统底层代码,执行速度十分快速
在分线程改完数据,将数据返回主存后,主线程都没有机会去获取主存中的数据
一直在执行while循环
两个线程都有独立的缓存
两者的数据不可见
产生了内存可见性问题:当多个线程访问共享数据时,彼此数据不可见
解决方法
使用同步锁,每次保证刷新缓存(synchronized)
synchronized (demo){
if(demo.isFlag()){
System.out.println("------");
break;
}
}
不过用到锁,效率马上就变低了
所以如何在不使用锁的情况下,解决数据可见性问题呢
volatile关键字:保证访问共享数据时,彼此数据是可见的
private volatile boolean flag = false;
使用volatile后,访问共享内存时,线程都回去主存中读取
效率肯定是降低了,不过相比锁的方式,更快了。
volatile真的就比synchronized就好吗
相较于synchronized,volatile是一种较为轻量级的同步策略
但是
1.volatile不具备“互斥性”
2.不能保证变量的“原子性”
2、Atomic类与原子可见性问题
package com.kj.test;
/**
* 研究原子性的问题
*
*/
public class TestAtomicDemo {
public static void main(String[] args) {
AtomicDemo atomicDemo = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(atomicDemo).start();
}
}
}
class AtomicDemo implements Runnable {
private int num = 0;
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + getNum());
}
public int getNum() {
return num++;
}
}
代码运行后,有些线程将读错数据。
我们是否可以用volatile解决这个问题呢?
不行。因为这里存在多个改写数据,不能保证i++的“原则性”,即过程不能分隔。这里多个线程仍然会出现读到的数据是别的线程已经改过的情况。
解决办法
jdk1.5以后 java.util.concurrent.atomic下提供了常用的原子变量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KwyVVQZg-1615708063502)(D:\Code\Markdown\image\juc801.png)]
这些变量都可以保证数据可见与原子性。原子变量中,封装的变量使用了volatile修饰。
并且使用CAS(compare-and-swap)算法保证数据的原子性。
CAS 算法是硬件对于并发操作共享数据的支持
CAS中包含三个操作数:
- 内存值 V
- 预估值 A
- 更新值 B
当且仅当 V == A 时,才将B的值赋给V,否则不做任何操作。所以当多个线程对共享数据进行修改时,只有一个线程会成功。这时线程在不能更新数据时,将不会阻塞,再执行一遍。
package com.kj.test;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 研究原子性的问题
* 解决办法:原子变量
* jdk1.5以后 java.util.concurrent.atomic下提供了常用的原子变量
*/
public class TestAtomicDemo {
public static void main(String[] args) {
AtomicDemo atomicDemo = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(atomicDemo).start();
}
}
}
class AtomicDemo implements Runnable {
private AtomicInteger integer = new AtomicInteger();
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + getNum());
}
public int getNum() {
return integer.getAndIncrement();
}
}
3、ConcurrentHashMap与其他集合类
HashTable是线程安全的,但是效率特别的低。并且对于一些特定的情况,是线程不安全的。
比如:
- 若不存在则添加
- 若存在则删除
if(!table.contants()){
table.put();
}
所以JDK1.5以后,提供了ConcurrentHashMap。采用了锁分段机制。默认有16个段Segment。
多个独立的锁,使得线程访问时,可以并行访问,不一定使用的是同一个段。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UHwAat9C-1615708063505)(D:\Code\Markdown\image\concurrentHahsMap.PNG)]
JDK1.8后将ConcurrentHashMap将分段锁取消了,换成了CAS。
原本线程安全的集合类,依然会保并发异常
package com.kj.test;
import java.util.*;
public class TestCopyOnWriteArrayList {
public static void main(String[] args) {
TestThread testThread = new TestThread();
for (int i = 0; i < 10; i++) {
new Thread(testThread).start();
}
}
}
class TestThread implements Runnable{
private static List<String> list = Collections.synchronizedList(new ArrayList<String>());
static{
list.add("AA");
list.add("BB");
list.add("CC");
}
public void run() {
Iterator<String> it = list.iterator();
//一遍迭代,一遍添加数据
while(it.hasNext()){
System.out.println(it.next());
list.add("AA");
}
}
}
java.util.ConcurrentModificationException并发修改异常。这里迭代和加入都是操作的同一个数据源。
iterator多线程修改异常:https://www.cnblogs.com/zhuyeshen/p/10956822.html
可以使用CopyOnWriteArrayList
private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
注意:CopyOnWriteArrayList会在每次写入时,都会进行一次复制,开销会很大。所以推荐在迭代次数多的情况下选择使用
4、CountDownLatch与闭锁
juc包中提供了多种并发容器类改进同步容器的性能。
CountDownLatch在完成某些运算时,只有其他线程的运算全部完成,当前运算才继续执行。
来一个情景,我们需要计算一个多线程程序执行的时间。
我们可能写出这样的代码
package com.kj.test;
import java.util.concurrent.CountDownLatch;
public class TestCountDownLatch {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
new Thread(demo).start();
}
long end = System.currentTimeMillis();
System.out.println("消耗时间为:" + (end - start));
}
}
class LatchDemo implements Runnable {
public void run() {
for (int i = 0; i < 50000; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
但是这个时间并不是我们想要的,这个是主线程执行结束的时间,其他的线程可能依然在执行。
我们希望主线程等其他线程都执行结束,才开始继续运行。
package com.kj.test;
import java.util.concurrent.CountDownLatch;
public class TestCountDownLatch {
public static void main(String[] args) {
//当CountDownLatch为0时,代表全部线程执行完
int threadNum = 10;
final CountDownLatch latch = new CountDownLatch(threadNum);
LatchDemo demo = new LatchDemo(latch);
long start = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
new Thread(demo).start();
}
try {
latch.await(); //latch不为0将一直等待
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("消耗时间为:" + (end - start));
}
}
class LatchDemo implements Runnable {
private CountDownLatch latch;
public LatchDemo(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
synchronized (this){
try{
for (int i = 0; i < 50000; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}finally {
//每一个线程执行完时,我们将CountDownLatch-1。
latch.countDown();
}
}
}
}
5、Callable与创建线程
这里我们提出新的一种创建线程的方式Callable。
我们可以与Runable接口进行对比下。
我们原本的方案:
class ThreadDemo1 implements Runnable{
public void run() {
}
}
现在我们使用Callable编写创建线程
package com.kj.test;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 创建执行线程的方式三:实现Callable接口
*/
public class _05_TestCreateThread {
public static void main(String[] args) {
TestCallable callable = new TestCallable();
// 相较与Runable方式,可以有返回值,并且可以抛出异常
// 执行Callable方式,需要FutureTask实现类的支持,用于接受运算结果。
// FutureTask是Future接口的实现类
FutureTask<Integer> res = new FutureTask(callable);
new Thread(res).start();
try {
// 在线程执行过程中,get并没有执行.
// 所以FutureTask 也可以用于闭锁
Integer sum = res.get();
System.out.println("-------"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class TestCallable implements Callable<Integer> {
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}
可以看到的是Callable
带有一个泛型。并且call()
方法带有返回值。
通过运行程序,查看结果我们可以知道在线程执行过程中,主线程中的get()方法并没有执行,而是等待整个其他线程执行结束,才执行。所以FutureTask 也可以用于闭锁
6、Lock同步锁
我们上锁的方法有三种
synchronized(隐式的锁),有jvm底层完成
- 同步代码块
- 同步方法
jdk1.5后:
- 同步锁Lock
Lock可以显示的上锁与开锁。通过lock()方法上锁,必须通过unlock()方法进行解锁。
unlock()通常放在finally中,避免遇到异常使程序逻辑异常。
package com.kj.test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class _06_TestLock {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "一号").start();
new Thread(ticket, "二号").start();
new Thread(ticket, "三号").start();
}
}
class Ticket implements Runnable {
private int tick = 100;
private Lock lock = new ReentrantLock();
public void run() {
while (tick > 0) {
lock.lock(); // 上锁
try {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "完成售票,余票为:" + --tick);
}finally {
lock.unlock(); // 释放锁
}
}
}
}
7、线程有序
需求:这里有三个线程,ID分别为ABC,每个线程在控制台打印自己ID,要求输出结果有序。
如:ABCABC…
package com.kj.test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class _09_TestABCThread {
public static void main(String[] args) {
final AlternateDemo demo = new AlternateDemo();
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 20; i++) {
demo.loopA(i);
}
}
}, "A").start();
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 20; i++) {
demo.loopB(i);
}
}
}, "B").start();
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 20; i++) {
demo.loopC(i);
}
}
}, "C").start();
}
}
class AlternateDemo {
// 当前正在执行线程标记
private int number = 1;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void loopA(int totalLoop) {
lock.lock();
try {
// 判断
if (number != 1) {
try {
condition1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i + "." + totalLoop);
}
number = 2;
condition2.signal();
} finally {
lock.unlock();
}
}
public void loopB(int totalLoop) {
lock.lock();
try {
// 判断
if (number != 2) {
try {
condition2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i + "." + totalLoop);
}
number = 3;
condition3.signal();
} finally {
lock.unlock();
}
}
public void loopC(int totalLoop) {
lock.lock();
try {
// 判断
if (number != 3) {
try {
condition3.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i + "." + totalLoop);
}
number = 1;
condition1.signal();
} finally {
lock.unlock();
}
}
}
我们需要三个进程间进行通信,在A打印完后通知B,并开锁
8、读写锁
对于写写、读写操作需要互斥
对于读读操作不需要互斥
ReadWirteLock维护了一对锁,一个读锁,一个写锁。写锁是独占的,而读锁是可以多个线程占有。
package com.kj.test;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class _10_TestReadWriteLock {
public static void main(String[] args) {
final ReadWriteDemo rw = new ReadWriteDemo();
new Thread(new Runnable() {
public void run() {
try {
//控制线程在中间写入
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
rw.wirte((int) (Math.random() * 10));
}
}, "write").start();
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
public void run() {
rw.read();
}
}).start();
}
}
}
class ReadWriteDemo {
private int number = 0;
private ReadWriteLock lock = new ReentrantReadWriteLock();
// 读
public void read() {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + ":" + number);
} finally {
lock.readLock().unlock();
}
}
// 写
public void wirte(int number) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName());
this.number = number;
} finally {
lock.writeLock().unlock();
}
}
}
9、线程八种锁
- 两个普通同步方法,两个线程,标准打印
package com.kj.test;
public class _11_TestThread8Monitor {
public static void main(String[] args) {
final Number num = new Number();
new Thread(new Runnable() {
public void run() {
num.getOne();
}
}).start();
new Thread(new Runnable() {
public void run() {
num.getTwo();
}
}).start();
}
}
class Number {
public synchronized void getOne() {
System.out.println("one");
}
public synchronized void getTwo() {
System.out.println("two");
}
}
打印:one two
- 新增Thread.sleep()给getOne()
package com.kj.test;
public class _11_TestThread8Monitor {
public static void main(String[] args) {
final Number num = new Number();
new Thread(new Runnable() {
public void run() {
num.getOne();
}
}).start();
new Thread(new Runnable() {
public void run() {
num.getTwo();
}
}).start();
}
}
class Number {
public synchronized void getOne() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public synchronized void getTwo() {
System.out.println("two");
}
}
打印:one two
- 新增普通方法getThree()
package com.kj.test;
public class _11_TestThread8Monitor {
public static void main(String[] args) {
final Number num = new Number();
new Thread(new Runnable() {
public void run() {
num.getOne();
}
}).start();
new Thread(new Runnable() {
public void run() {
num.getTwo();
}
}).start();
new Thread(new Runnable() {
public void run() {
num.getThree();
}
}).start();
}
}
class Number {
public synchronized void getOne() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public synchronized void getTwo() {
System.out.println("two");
}
public void getThree() {
System.out.println("three");
}
}
打印:three one two
- 两个普通同步方法,两个Number对象
package com.kj.test;
public class _11_TestThread8Monitor {
public static void main(String[] args) {
final Number num1 = new Number();
final Number num2 = new Number();
new Thread(new Runnable() {
public void run() {
num1.getOne();
}
}).start();
new Thread(new Runnable() {
public void run() {
num2.getTwo();
}
}).start();
}
}
class Number {
public synchronized void getOne() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public synchronized void getTwo() {
System.out.println("two");
}
}
打印:two one
- static修饰getOne(),一个Number对象
package com.kj.test;
public class _11_TestThread8Monitor {
public static void main(String[] args) {
final Number num1 = new Number();
//final Number num2 = new Number();
new Thread(new Runnable() {
public void run() {
num1.getOne();
}
}).start();
new Thread(new Runnable() {
public void run() {
num1.getTwo();
//num2.getTwo();
}
}).start();
}
}
class Number {
public static synchronized void getOne() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public synchronized void getTwo() {
System.out.println("two");
}
public void getThree() {
System.out.println("three");
}
}
打印:two one
- static修饰getOne()、getTwo(),一个Number对象
package com.kj.test;
public class _11_TestThread8Monitor {
public static void main(String[] args) {
final Number num1 = new Number();
//final Number num2 = new Number();
new Thread(new Runnable() {
public void run() {
num1.getOne();
}
}).start();
new Thread(new Runnable() {
public void run() {
num1.getTwo();
//num2.getTwo();
}
}).start();
}
}
class Number {
public static synchronized void getOne() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public static synchronized void getTwo() {
System.out.println("two");
}
public void getThree() {
System.out.println("three");
}
}
打印:one two
- 一个静态同步方法,一个非静态同步方法;两个Number对象,一个GetOne、一个GetTwo
package com.kj.test;
public class _11_TestThread8Monitor {
public static void main(String[] args) {
final Number num1 = new Number();
final Number num2 = new Number();
new Thread(new Runnable() {
public void run() {
num1.getOne();
}
}).start();
new Thread(new Runnable() {
public void run() {
//num1.getTwo();
num2.getTwo();
}
}).start();
}
}
class Number {
public static synchronized void getOne() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public synchronized void getTwo() {
System.out.println("two");
}
public void getThree() {
System.out.println("three");
}
}
打印:two one
- 两个静态同步方法;两个Number对象,一个GetOne、一个GetTwo
package com.kj.test;
public class _11_TestThread8Monitor {
public static void main(String[] args) {
final Number num1 = new Number();
final Number num2 = new Number();
new Thread(new Runnable() {
public void run() {
num1.getOne();
}
}).start();
new Thread(new Runnable() {
public void run() {
//num1.getTwo();
num2.getTwo();
}
}).start();
}
}
class Number {
public static synchronized void getOne() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public static synchronized void getTwo() {
System.out.println("two");
}
public void getThree() {
System.out.println("three");
}
}
打印:one two
线程八锁关键:
- 非静态方法的锁为 this,静态方法锁为对应的Class实例
- 在某个时刻内,只有某一个线程持有锁,无论是有几个方法
10、线程池
线程池提供了一个线程队列,线程池中保存了所有等待状态的线程
java.util.concurrent.Executor:负责线程的使用与调度的根接口
|–ExecutorService 子接口:线程池的主要接口
|–ThreadPoolExecutor 线程池的实现类
|–ScheduledExecutorService 子接口:负责线程调度
|–ScheduledThreadPoolExecutor:继承了ThreadPoolExecutor ,实现了ScheduledExecutorService
工具类:Executors
ExecutorService newFixedThreadPool(int size) 创建固定大小的连接池
ExecutorService newCachedThreadPool() 缓存线程池,数量不固定,可以根据需求自动的更改数据
ExecutorService newSingleThreadExecutor() 创建单个线程池。线程池中只有一个线程
ScheduledExecutorService newScheduledThreadPool() 创建固定大小的线程池,延迟或者定时的执行任务
package com.kj.test;
import java.util.concurrent.*;
public class _12_TestThreadPool {
public static void main(String[] args) {
// 1. 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 2.为线程池中的线程分配任务
ThreadPoolDemo demo = new ThreadPoolDemo();
for (int i = 0; i < 10; i++) {
//executorService.submit(demo);
Future<Integer> res = executorService.submit(new Callable<Integer>() {
public Integer call() throws Exception {
int i = 0;
while (i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + i++);
}
return i;
}
});
try {
System.out.println(res.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
// 3. 关闭线程池。shutdown等待所有线程池中的任务都关闭后,再关闭
executorService.shutdown();
}
}
class ThreadPoolDemo implements Runnable {
private int i = 0;
public void run() {
while (i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + i++);
}
}
}
使用线程调度ScheduledExecutorService
package com.kj.test;
import java.util.Random;
import java.util.concurrent.*;
/**
* 调度
*/
public class _13_TestScheduledThreadPool {
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
// 延迟三秒执行Callable
for (int i = 0; i < 10; i++) {
Future<Integer> future = pool.schedule(new Callable<Integer>() {
public Integer call() throws Exception {
int num = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName() + ":" + num);
return num;
}
}, 1, TimeUnit.SECONDS);
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
pool.shutdown();
}
}
11、分支合并ForkJoinPool
可以将一个大任务拆分成一个个小任务,然后将小任务的结果一个个合并。
采用“工作窃取”模式(work-stealing) :当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。
拆分和合并是需要时间的,如果工程量不大,可以直接运算。
package com.kj.test;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
public class _14_TestForkJoinPool {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Long> demo = new ForkJoinDemo(0L, 100000000L);
Long sum = pool.invoke(demo);
System.out.println(sum);
}
}
class ForkJoinDemo extends RecursiveTask<Long> {
private long start;
private long end;
private static final long THRSHOLD = 10000L; //临界值
public ForkJoinDemo(long start, long end) {
this.start = start;
this.end = end;
}
protected Long compute() {
long length = end - start;
if (length <= THRSHOLD) {
long sum = 0L;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
long middle = (start + end) / 2;
ForkJoinDemo culculate1 = new ForkJoinDemo(start, middle);
culculate1.fork(); //进行拆分,同时加入线程队列
ForkJoinDemo culculate2 = new ForkJoinDemo(middle + 1, end);
culculate2.fork();
//合并
return culculate1.join() + culculate2.join();
}
}
}