在Java 5.0 提供了java.util.concurrent(简称JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的Collection 实现等。
volatile 关键字 内存可见性
package cn.fg.test03;
public class Tester {
public static void main(String[] args) {
Hello hello = new Hello();
new Thread(hello).start();
while (true) {
if (hello.isFlag()) {
System.out.println("----------------------------");
break;
}
}
}
}
class Hello implements Runnable {
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000); //模拟逻辑处理睡1秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
flag = true; //业务逻辑处理完置标志
System.out.println(flag);
}
public boolean isFlag() {
return flag;
}
}
通过执行上述代码,你会发现while循环永远不会break,main线程会一直等待。为什么?hello线程花了1秒来处理逻辑,然后设置的flag的值,main线程进入while时flag为false,由于while底层效率非常高,main线程永远读不到true,如果你在while外或里面也睡一会,就会读到flag=true了。
如何解决?加上synchronized代码块即可
public static void main(String[] args) {
Hello hello = new Hello();
new Thread(hello).start();
while (true) {
synchronized (hello) {
if (hello.isFlag()) {
System.out.println("----------------------------");
break;
}
}
}
}
虽然synchronized可以解决,但是不是最优选择。Java提供了一种稍弱的同步机制,即volatile 变量,用来确保将变量的更新操作通知到其他线程。可以将volatile 看做一个轻量级的锁,但是又与锁有些不同:对于多线程,不是一种互斥关系;不能保证变量状态的“原子性操作”。说简单点,volatile 只能解决内存可见,不能解决线程安全。
//volatile 解决内存可见问题,加上volatile后,会实时更新主存,没有了缓存,效率要稍差些
//加与不加,具体看业务场景
private volatile boolean flag = false;
Atomic 原子变量
还是先来看一段代码
package cn.fg.test03;
public class TestAtomicDemo {
public static void main(String[] args) {
AtomicDemo ad = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(ad).start();
}
}
}
class AtomicDemo implements Runnable{
private int serialNumber = 0;
@Override
public void run() {
serialNumber++; //自加1
System.out.println(serialNumber);
}
}
在做++操作的时候,会有线程安全问题,可以使用synchronized来解决;这里我们使用另外一种解决方式,使用AtomicXXX原子变量。为什么叫原子变量?我们以++语法为例,++操作实际是做了以下几步:
1. int temp = serialNumber ;
2. serialNumber = serialNumber + 1;
3. serialNumber = temp;
可以简单理解为事务,读、写、改必须一步完成,不能被其他操作修改其值;原子变量底层使用了CAS算法
所以在java.util.concurrent.atomic包下为我们提供了原子类,这里我们使用AtomicInteger来修改代码
package cn.fg.test03;
import java.util.concurrent.atomic.AtomicInteger;
public class TestAtomicDemo {
public static void main(String[] args) {
AtomicDemo ad = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(ad).start();
}
}
}
class AtomicDemo implements Runnable{
private AtomicInteger serialNumber = new AtomicInteger(0);
@Override
public void run() {
System.out.println(serialNumber.getAndIncrement());
}
}
使用原子变量的效率比synchronized高,synchronized可以看成是悲观锁,Atomic可以看成是乐观锁。
CopyOnWriteArrayList 写入并复制
写入并复制,该list解决的是arraylist在add()的时候线程安全的问题,那么arraylist到底有什么线程安全问题呢?
package cn.fg.test04;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Tester {
public static void main(String[] args) {
Hello hello = new Hello();
for (int i = 0; i < 100 ; i++) {
new Thread(hello).start();
}
}
}
class Hello implements Runnable {
//private List<String> list = Collections.synchronizedList(new ArrayList<String>());
private ArrayList<String> list = new ArrayList<String>();
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//此处会发生数组越界,下标的值被其他线程覆盖的问题
//更多线程安全问题 https://blog.csdn.net/u010416101/article/details/88720974
list.add(Thread.currentThread().getName());
}
}
//使用CopyOnWriteArrayList(写入并复制)替换ArrayList
//虽然CopyOnWriteArrayList是线程安全的,但是频繁add效率很低,因为每次add的时候都会copy一个新的list进行add
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
CountDownLatch 闭锁
我们现在有这样一个场景,在主线程中开启5个线程分别去做不同的事,然后统计5个线程的总用时。
package cn.fg.juc1;
import java.util.concurrent.CountDownLatch;
public class TestCountDownLatch {
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2); //构造参数是计数器
long start = System.currentTimeMillis();
new Thread(new DoOne(latch)).start(); //做第一件事
new Thread(new DoTow(latch)).start(); //做第二件事
try {
latch.await(); //等待阻塞中,计数器为0时才会执行后面的代码
} catch (InterruptedException e) {
}
long end = System.currentTimeMillis();
System.out.println("耗费时间为:" + (end - start));
}
}
//做第一件事
class DoOne implements Runnable {
private CountDownLatch latch;
public DoOne(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
System.out.println("DoOne.....................done"); //模拟做完了
} finally {
//做完这件事后,要调用一下countDown(),计数器才会-1,因此要保证该条语句一定要被执行到,否则main线程就会一直阻塞
//该方法是线程安全的
latch.countDown();
}
}
}
//做第二件事
class DoTow implements Runnable {
private CountDownLatch latch;
public DoTow(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
Thread.sleep(5000);
System.out.println("DoTow.....................done"); //模拟做完了
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}
}
另外callabel接口的线程也能实现该功能
//class NumThread implements Callable<Integer>
NumThread numThread = new NumThread();
FutureTask<Integer> futureTask = new FutureTask<Integer>(numThread); //callable实现类需要与
new Thread(futureTask).start();
Integer sum = futureTask.get();
Lock锁的使用
java.util.concurrent.locks.Lock 接口是 控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象
1. ReentrantLock
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 在 实现线程安全的控制中,比较常用的是 ReentrantLock 可以显式加锁、释放锁 。
package cn.fg.test02;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* lock锁的线程通信例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
*
*/
class Number implements Runnable {
private int number = 1;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition(); //等同于synchronized的监视器对象
@Override
public void run() {
while (true) {
try {
lock.lock();
condition.signal(); //等同于this.notify()
//condition.signalAll(); //等同于this.notifyAll()
if (number <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
condition.await(); //等同于this.wait()
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
2. Condition 线程通讯
Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个Lock 可能与多个Condition 对象关联
package cn.fg.test02;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* lock锁的线程通信例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
*
*/
class Number implements Runnable {
private int number = 1;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition(); //等同于synchronized的监视器对象
@Override
public void run() {
while (true) {
try {
lock.lock();
condition.signal(); //等同于notify()
//condition.signalAll(); //等同于notifyAll()
if (number <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
condition.await(); //等同于wait()
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
package cn.fg.test02;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
* 编写一个程序,开启 3个线程ABC,实现轮流打印 ,例如:ABCABCABCABC
*/
public class TestABCAlternate {
public static void main(String[] args) {
AlternateDemo ad = new AlternateDemo();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
ad.printA();
}
}
}, "A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
ad.printB();
}
}
}, "B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
ad.printC();
System.out.println("-----------------------------------");
}
}
}, "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 printA(){
lock.lock();
try {
//1. 判断
if(number != 1){
condition1.await(); //A线程等待
}
//2. 打印
System.out.print(Thread.currentThread().getName());
//3. 唤醒B线程
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
//1. 判断
if(number != 2){
condition2.await(); //B线程等待
}
//2. 打印
System.out.print(Thread.currentThread().getName());
//3. 唤醒C线程
number = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
//1. 判断
if(number != 3){
condition3.await(); //C线程等待
}
//2. 打印
System.out.print(Thread.currentThread().getName());
//3. 唤醒A线程
number = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
ReadWriteLock 读写锁
读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,我们只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
读写锁接口:ReadWriteLock,它的具体实现类为:ReentrantReadWriteLock
package cn.fg.test04;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/*
* 1. ReadWriteLock : 读写锁
*
* 写写/读写 需要“互斥”
* 读读 不需要互斥
*/
public class TestReadWriteLock {
public static void main(String[] args) {
ReadWriteLockDemo rw = new ReadWriteLockDemo();
//一个线程用来写数据
new Thread(new Runnable() {
@Override
public void run() {
rw.set((int)(Math.random() * 101));
}
}, "Write:").start();
//两个线程用来读取数据
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
rw.get();
}
}).start();
}
}
}
class ReadWriteLockDemo{
private int number = 0;
private ReadWriteLock lock = new ReentrantReadWriteLock();
//读
public void get(){
//上读锁,上锁后,若其他线程为写锁,根据“写写/读写”互斥原则,其他线程需要等待该读线程执行完毕后才可以执行;若其他线程为读锁或无锁,则不会发生任何影响
lock.readLock().lock();
try{
System.out.println(Thread.currentThread().getName() + " : " + number);
}finally{
lock.readLock().unlock(); //释放读锁
}
}
//写
public void set(int number){
System.out.println("准备写入数据.....................");
lock.writeLock().lock(); //上写锁
//为了演示读写锁,这里我们将写数据睡1秒。3个线程同时执行,若先执行的是写线程,则两个读线程将会等待,待写线程释放锁后,根据读读不互斥原则,两个读线程不会发生等待,会同时执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try{
System.out.println("写入完毕");
this.number = number;
}finally{
lock.writeLock().unlock(); //释放写锁
}
}
}
https://www.cnblogs.com/liang1101/p/6475555.html?utm_source=itdadao&utm_medium=referral
线程八锁
下面示例是线程锁的实践,多个线程执行不同的带synchronized 的方法时,并不一定是同时执行的,关键是搞清楚执行线程中持有的锁是否是同一把锁,若是同一把锁,当其中一个锁被锁住时,其他线程则进入阻塞
package cn.fg.test04;
/*
* 题目:判断打印的 "one" or "two" ?
*
* 1. 两个普通同步方法,两个线程,标准打印, 打印? //one two
* 2. 新增 Thread.sleep() 给 getOne() ,打印? //one two
* 3. 新增普通方法 getThree() , 打印? //three one two
* 4. 两个普通同步方法,两个 Number 对象,打印? //two one
* 5. 修改 getOne() 为静态同步方法,打印? //two one
* 6. 修改两个方法均为静态同步方法,一个 Number 对象? //one two
* 7. 一个静态同步方法,一个非静态同步方法,两个 Number 对象? //two one
* 8. 两个静态同步方法,两个 Number 对象? //one two
*
* 线程八锁的关键:
* ①非静态方法的锁默认为 this, 静态方法的锁为 对应的 Class 实例
* ②某一个时刻内,只能有一个线程持有锁,无论几个方法。
*/
public class TestThread8Monitor {
public static void main(String[] args) {
Number number = new Number();
Number number2 = new Number();
new Thread(new Runnable() {
@Override
public void run() {
number.getOne();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// number.getTwo();
number2.getTwo();
}
}).start();
/*new Thread(new Runnable() {
@Override
public void run() {
number.getThree();
}
}).start();*/
}
}
class Number{
public static synchronized void getOne(){//Number.class
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
System.out.println("one");
}
public synchronized void getTwo(){//this
System.out.println("two");
}
public void getThree(){
System.out.println("three");
}
}
newScheduledThreadPool 调度线程池
package cn.fg.test04;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/*
* 一、线程池:提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提高了响应的速度。
*
* 二、线程池的体系结构:
* java.util.concurrent.Executor : 负责线程的使用与调度的根接口
* |--**ExecutorService 子接口: 线程池的主要接口
* |--ThreadPoolExecutor 线程池的实现类
* |--ScheduledExecutorService 子接口:负责线程的调度
* |--ScheduledThreadPoolExecutor :继承 ThreadPoolExecutor, 实现 ScheduledExecutorService
*
* 三、工具类 : Executors
* ExecutorService newFixedThreadPool() : 创建固定大小的线程池
* ExecutorService newCachedThreadPool() : 缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
* ExecutorService newSingleThreadExecutor() : 创建单个线程池。线程池中只有一个线程
*
* ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务。
*/
public class TestScheduledThreadPool {
public static void main(String[] args) throws Exception {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
for (int i = 0; i < 5; i++) {
pool.schedule(new Runnable() {
@Override
public void run() {
int num = new Random().nextInt(100);//生成随机数
System.out.println(Thread.currentThread().getName() + " : " + num);
}
}, 1, TimeUnit.SECONDS); //延迟1秒钟执行,TimeUnit.SECONDS 可以更改延迟的单位,秒、小时、天等
}
pool.shutdown();
}
}
更多调度线程用法 https://blog.csdn.net/weixin_39792935/article/details/105357000
Fork/Join 框架
Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行join 汇总。
Fork/Join 框架与线程池的区别
采用“工作窃取”模式(work-stealing):
当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。
相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能。
package cn.fg.juc2;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;
import org.junit.Test;
//从0累加到500亿,拆分若干任务的的示例
public class TestForkJoinPool {
public static void main(String[] args) {
Instant start = Instant.now();
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinSumCalculate(0L, 50000000000L);
Long sum = pool.invoke(task);
System.out.println(sum);
Instant end = Instant.now();
System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());//166-1996-10590
}
@Test
public void test1(){
Instant start = Instant.now();
long sum = 0L;
for (long i = 0L; i <= 50000000000L; i++) {
sum += i;
}
System.out.println(sum);
Instant end = Instant.now();
System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());//35-3142-15704
}
//java8 新特性
@Test
public void test2(){
Instant start = Instant.now();
Long sum = LongStream.rangeClosed(0L, 50000000000L)
.parallel()
.reduce(0L, Long::sum);
System.out.println(sum);
Instant end = Instant.now();
System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());//1536-8118
}
}
class ForkJoinSumCalculate extends RecursiveTask<Long>{
/**
*
*/
private static final long serialVersionUID = -259195479995561737L;
private long start;
private long end;
private static final long THURSHOLD = 10000L; //临界值
public ForkJoinSumCalculate(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long length = end - start;
if(length <= THURSHOLD){
long sum = 0L;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
}else{
long middle = (start + end) / 2;
ForkJoinSumCalculate left = new ForkJoinSumCalculate(start, middle);
left.fork(); //进行拆分,同时压入线程队列
ForkJoinSumCalculate right = new ForkJoinSumCalculate(middle+1, end);
right.fork(); //
return left.join() + right.join();
}
}
}