概述
什么是JUC包?
JUC包:jdk中java.util.concurrent包,从jdk1.5后开始出现,用来解决java中多线程编程问题
在学习之前先复习一下计算机操作系统进程和线程之间的区别:
进程:系统进行资源调度的基本单位。
线程:CPU进行资源调度的基本单位。
创建线程的四种方式
1)继承线程Thread类
2)实现Runable接口(常用线程实现方法)
3)实现Callable接口(这里常用于有返回值的线程实现)
4)线程池
Case 1 继承线程Thread类创建线程:
public class MyThread extends Thread{
@Override
/*
* 重新给Thread类中的run方法
* */
public void run()
{
System.out.println(MyThread.currentThread().getName()+":继承Thread线程类实现的线程....");
}
public static void main(String[] args)
{
Thread thread = new MyThread();
thread.start();
}
}
Case 2 实现Runnable接口创建线程:
public class MyThread2 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":实现Runnable接口实现的线程....");
}
public static void main(String[] args)
{
/*
* 把Runnable实例传进Thread类作为其构造方法的参数
* */
Thread thread = new Thread(new MyThread2());
thread.start();
}
}
Case 3 实现Callable接口创建线程:
要用到FutureTask去取线程的返回值:
/*
*@Description:启动一个线程,将一个变量创建去然后进行加1操作后获取其返回值
* 取返回值要用FutureTask
* */
public class MyThread3 implements Callable<Integer> {
int value;
public MyThread3(int value)
{
this.value = value;
}
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+":实现Callable接口实现的线程");
return ++value;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
int InitValue = 1;
FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread3(InitValue));
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("初始值:"+InitValue+",线程执行后获取返回的值:"+futureTask.get());
}
}
线程的状态
在java.lang.Thread类下有个枚举类State,里面定义了线程的所有状态,一共6个状态(源码在Column1742):
1)NEW 新建状态,线程创建且没有执行start方法时的状态
2)RUNNABLE 运行状态,线程已经启动,但是等待相应的资源(比如IO或者时间片切换)才能开始执行
3)BLOCKED 阻塞状态,阻塞状态,当遇到synchronized或者lock且没有取得相应的锁,就会进入这个状态
4)WATTING 等待状态,当调用Object.wait或者Thread.join()且没有设置时间,在或者LockSupport.park时,都会进入等待状态。
5)TIMED_WAITING 计时等待状态,当调用Thread.sleep()或者Object.wait(xx)或者Thread.join(xx)或者LockSupport.parkNanos或者LockSupport.partUntil时,进入该状态
6)TERMINATED 终结状态,线程中断或者运行结束的状态
模拟并发Demo
售票Demo,模拟并发两种实现方式(防止超卖)
1) 加synchronized 关键字
/*
* 创建一个资源类,表示票数
* */
class Ticket{
//表示有100张票可以卖
private static int numbers = 100;
//定义卖票的行为方法,加synchronzied 关键字
public static synchronized void sellTicket()
{
if(numbers>0)
{
numbers --;
System.out.println(Thread.currentThread().getName()+":卖出一张票,还剩"+numbers+"张");
}
else{
System.out.println(Thread.currentThread().getName()+":票售完......");
}
}
}
class Sailor implements Runnable{
public void run()
{ for(int i=0;i<50;i++)
{
Ticket.sellTicket();
}
}
}
public class TicketSell {
//3个售票线程模拟并发
public static void main(String[] args) {
Thread thread1 = new Thread(new Sailor(), "售票窗口1");
Thread thread2 = new Thread(new Sailor(), "售票窗口2");
Thread thread3 = new Thread(new Sailor(), "售票窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
2)用ReentrantLock
import java.util.concurrent.locks.ReentrantLock;
/*
* 创建一个资源类,表示票数
* */
class Ticket{
//表示有100张票可以卖
private static int numbers = 100;
static ReentrantLock lock = new ReentrantLock();
//定义卖票的行为方法,加synchronzied 关键字
public static void sellTicket()
{ lock.lock();
if(numbers>0)
{
numbers --;
System.out.println(Thread.currentThread().getName()+":卖出一张票,还剩"+numbers+"张");
}
else{
System.out.println(Thread.currentThread().getName()+":票售完......");
}
lock.unlock();
}
}
class Sailor implements Runnable{
public void run()
{ for(int i=0;i<50;i++)
{
Ticket.sellTicket();
}
}
}
public class TicketSell {
public static void main(String[] args) {
Thread thread1 = new Thread(new Sailor(), "售票窗口1");
Thread thread2 = new Thread(new Sailor(), "售票窗口2");
Thread thread3 = new Thread(new Sailor(), "售票窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
加减Demo,共享一个资源类,模拟并发进行对其修改
1) 加synchronized 关键字
/*
*定义一个共享资源类
*/
class ShareResource {
private int value = 0;
public synchronized void add() throws InterruptedException {
while (value != 0) {
this.wait();
}
value++;
this.notifyAll();
System.out.println(Thread.currentThread().getName()+":"+value);
}
public synchronized void sub() throws InterruptedException {
while (value != 1) {
this.wait();
}
value--;
this.notifyAll();
System.out.println(Thread.currentThread().getName()+":"+value);
}
}
public class ThreadDemo {
public static void main(String[] args)
{
ShareResource resource = new ShareResource();
new Thread(()->{
try {
for(int i=0;i<10;i++)
resource.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"加法线程").start();
new Thread(()->{
try {
for(int i=0;i<10;i++)
resource.sub();
}catch (InterruptedException e)
{
e.printStackTrace();
}
},"减法线程").start();
}
}
2)用ReentrantLock
/*
*定义一个共享资源类
*/
class ShareResource {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private int value = 0;
public void add() throws InterruptedException {
lock.lock();
try{
while (value != 0) {
condition.await();
}
value++;
System.out.println(Thread.currentThread().getName()+":"+value);
condition.signalAll();}
finally{
lock.unlock();
}
}
public void sub() throws InterruptedException {
lock.lock();
try{
while (value != 1) {
condition.await();
}
value--;
System.out.println(Thread.currentThread().getName()+":"+value);
condition.signalAll();
}finally {
lock.unlock();
}
}
}
public class ThreadDemo {
public static void main(String[] args)
{
ShareResource resource = new ShareResource();
new Thread(()->{
try {
for(int i=0;i<10;i++)
resource.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"加法线程").start();
new Thread(()->{
try {
for(int i=0;i<10;i++)
resource.sub();
}catch (InterruptedException e)
{
e.printStackTrace();
}
},"减法线程").start();
}
}
总结:加synchronized关键字不用手动解锁,而如果用ReentrantLock 则要手动解锁,不然该线程会一直锁定该资源,其他线程无法进行访问
线程间的定制化通信
概述:启动三个线程,要求如下:
线程1打印5次,线程2打印10次,线程3打印15次依次类推
class ShareResource{
//flag=1,2,3
~~int flag =1;
ReentrantLock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition()~~ ;
public void print5(int loop) throws InterruptedException {
lock.lock();
while(flag!=1)
{
c1.await();
}
for(int i=1;i<=5;i++)
{ System.out.println(Thread.currentThread().getName()+"::"+i+",loop:"+loop);}
flag = 2;
c2.signal();
lock.unlock();
}
public void print10(int loop) throws InterruptedException {
lock.lock();
while(flag!=2)
{
c2.await();
}
for(int i=1;i<=10;i++)
{ System.out.println(Thread.currentThread().getName()+"::"+i+",loop:"+loop);}
flag = 3;
c3.signal();
lock.unlock();
}
public void print15(int loop) throws InterruptedException {
lock.lock();
while(flag!=3)
{
c3.await();
}
for(int i=1;i<=15;i++)
{ System.out.println(Thread.currentThread().getName()+"::"+i+",loop:"+loop);}
flag=1;
c1.signal();
lock.unlock();
}
}
public class ThreadDemo {
public static void main(String[] args)
{
ShareResource resource = new ShareResource();
new Thread(()->{
for(int i=1;i<=5;i++) {
try {
resource.print5(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for(int i=1;i<=5;i++) {
try{
resource.print10(i);
}catch (InterruptedException e)
{e.printStackTrace();}
}
},"BB").start();
new Thread(()->{
for(int i=1;i<=5;i++) {
try{
resource.print15(i);
}catch (InterruptedException e)
{e.printStackTrace();}
}
},"CC").start();
}
}
集合类的线程安全问题
ArrayList的线程安全问题
ArrayList线程的不安全演示:
ArrayList源码的add方法(Column 463)方法上没有加synchronized关键字
ArrayList的add方法
public class ThreadDemo {
public static void main(String[] args)
{
ArrayList<String> list = new ArrayList<>();
for(int i=0;i<30;i++)
{
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
出现异常:
解决方案-Vector
Vector的add方法(Column 785):
public class ThreadDemo {
public static void main(String[] args)
{
Vector<String> vector = new Vector<>();
for(int i=0;i<30;i++)
{
new Thread(()->{
vector.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(vector);
},String.valueOf(i)).start();
}
}
}
Vector 方法不常用,jdk1.0就已经出现
解决方案-Collections
查看源码可以知道
这里的add方法出现了一个mutex,这个是什么呢?继续找到他继承的类SynchronizedCollection,可以知道mutex是一个Object对象,而且构造方法中,mutex就是我们的list,所以Collection是对整个链表进行加锁
public class ThreadDemo {
public static void main(String[] args)
{ List<String> arrayList = new ArrayList<>();
List<String> list = Collections.synchronizedList(arrayList);
for(int i=0;i<30;i++)
{
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
但是这种也不太常用
解决方案-CopyOnWriteArrayList(写时复制技术)
源码:(column 434)
public class ThreadDemo {
public static void main(String[] args)
{ List<String> list = new CopyOnWriteArrayList<>();
for(int i=0;i<30;i++)
{
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
支持并发读,独立写。解决了并发修改的问题
先复制一个跟原来集合一样的内容,之前的人仍然读之前的那份,写完之后再去跟之前的内容进行一个合并
HashSet的线程安全问题
可以看到HashSet集合的add方法也是没有加synchronized关键字的,仍旧会出现并发修改问题
同样Set也有 CopyOnWriteArraySet 去实现线程安全
HashSet的底层就是一个HashMap
public class ThreadDemo {
public static void main(String[] args)
{
CopyOnWriteArraySet set = new CopyOnWriteArraySet(new HashSet<>());
for(int i=0;i<30;i++)
{
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
HashMap 线程安全问题
源码:put方法也是没有synchronized关键字
解决办法:ConcurrentHashMap
public class ThreadDemo {
public static void main(String[] args)
{
Map<String,String> map = new ConcurrentHashMap<>();
for(int i=0;i<30;i++)
{ String key = String.valueOf(i);
new Thread(()->{
map.put(key,UUID.randomUUID().toString().substring(0,8));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
多线程锁
公平锁和非公平锁
在ReetranLock的构造方法中有个boolean字段的fair参数,当为true时,表明当前锁是公平锁,反之为非公平锁
源码:
class ShareResource{
//可重入锁默认是非公平锁,不管该资源有没有被占用,先到线程都会直接进入抢占
//公平锁,在抢占时,会先询问,该资源有没有被占用
//剩余打印次数
int num = 10;
private final ReentrantLock lock = new ReentrantLock(false);
public void print()
{ lock.lock();
if(num>0)
{
System.out.println(Thread.currentThread().getName()+"::"+"执行了打印方法");
num--;
}
lock.unlock();
}
}
public class ThreadDemo {
public static void main(String[] args)
{
ShareResource resource = new ShareResource();
new Thread((new Runnable() {
@Override
public void run() {
for(int i=0;i<50;i++) {
resource.print();
}
}
}),"A线程").start();
new Thread((new Runnable() {
@Override
public void run() {
for(int i=0;i<50;i++) {
resource.print();
}
}
}),"B线程").start();
new Thread((new Runnable() {
@Override
public void run() {
for(int i=0;i<50;i++) {
resource.print();
}
}
}),"C线程").start();
}
}
非公平锁,可以看到A线程抢先进入了共享资源,独占了共享资源,而其他线程会有饿死的情况发生
公平锁,其他线程均有执行,解决了线程饿死的问题,在共享资源少的情况下,仍然可能会出现和上面非公平锁一样的情况,这是因为创建线程需要时间,可以把共享资源的值调大
Synchronized关键字锁的情况
主要分清锁的是哪个,是对象还是整个Class,静态方法锁的是整个Class
//共享资源类里面有两个方法,一个打印的是MethodA,一个打印的是MethodB
class ShareResource{
public synchronized void MethodA()
{
System.out.println("MethodA......");
}
public synchronized void MethodB()
{
System.out.println("MethodB......");
}
}
public class ThreadDemo {
public static void main(String[] args)
{
ShareResource resource = new ShareResource();
new Thread(()->{
resource.MethodA();
},"线程A").start();
new Thread(()->{
resource.MethodB();
},"线程B").start();
}
}
可重入锁
synchronied(隐式的可重入锁)和 ReentrantLock(显式)都是可重入锁,也叫递归锁
隐式:不用手动加锁和解锁
显式:需要手动加锁和解锁
1、可以自由进入的锁,可以用同步代码块,或者同步方法验证synchronied的可重入特点
2、ReentrantLock虽然可以在进行递归上锁的时候不解锁,这样虽然不会影响本个线程进行,但是由于没有解锁,会影响其他线程获取该锁,使另外线程需要该资源的一直在等待解锁。
死锁
1、什么是死锁?
两个或者两个以上的进程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们无法再执行下去。
class ShareResource{
private ShareResource shareResource;
public void setShareResource(ShareResource shareResource)
{
this.shareResource = shareResource;
}
public synchronized void print() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"::"+"持有了本对象的锁");
System.out.println(Thread.currentThread().getName()+"::"+"等待获取另外的锁");
Thread.sleep(100);
//尝试获取B线程的锁
shareResource.print();
System.out.println(Thread.currentThread().getName()+"::"+"获取了另外的锁....");
}
}
public class ThreadDemo {
public static void main(String[] args)
{
ShareResource resource1 = new ShareResource();
ShareResource resource2 = new ShareResource();
resource1.setShareResource(resource2);
resource2.setShareResource(resource1);
new Thread(()->{
try {
resource1.print();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程A").start();
new Thread(()->{
try {
resource2.print();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程B").start();
}
}
可以看到两个线程,都在等待对方释放锁,一直循环等待就处于一个死锁的状态
2、产生死锁的原因:
1)系统资源不足
2)进程运行推进的顺序不合适
3)资源分配不当
3、验证是否是死锁
(1)jps命令 类似linux ps -ef
(2)jstack 查看堆栈信息,跟踪堆栈信息,jvm里面自带堆栈跟踪工具
先在java的bin目录下,执行jps -l
找到该程序的pid号,图中7476就是本程序的进程号
再进行jstack [pid] 命令查看堆栈信息:
可以发现在信息的最后面发现有一行信息 Found 1 deadlock,就是发现一个死锁
Callable接口
复习线程创建的四种方式:在文章目录中有
Runnable 接口 和 Callable 接口对比
一、是否有返回值(Runnable接口没有,Callable接口有)
二、是否抛出异常(Callable如果没法返回计算结果,会抛出异常)
三、实现方法名称不同(Runnable要实现的方法是Run,Callable实现的方法是Call)
Callable接口不能直接替换Runable接口去创建线程,因为线程的构造方法里面只有Runnable接口,所以找一个类,和Runnable接口有关系,又和Callable接口也有关系
Runnable 接口有实现类FutureTask
FutureTask构造可以传递Callable
JUC强大的辅助类
减少计数CountDownLatch
1、CountDownLatch 类可以设置一个计数器,然后通过countDown方法来进行减1的操作,使用await方法等待计数器不大于0,然后继续执行await方法之后的语句
2、CountDownLatch 主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞
其它线程调用countDown 方法会将计数器减1(调用countDown方法的线程不会阻塞)
3、当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
验证demo
/*
* 6个同学陆续离开教室后值班的同学才可以关门
* */
class ClassRoom{
CountDownLatch countDownLatch = new CountDownLatch(6);
public void leave()
{ this.countDownLatch.countDown();
System.out.println(Thread.currentThread().getName()+"离开教室.....");
}
public void close() throws InterruptedException {
this.countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"关门....");
}
}
public class ThreadDemo {
public static void main(String[] args) {
ClassRoom classRoom = new ClassRoom();
new Thread((() -> {
try {
classRoom.close();
} catch (InterruptedException e) {
e.printStackTrace();
}
}), "值班学生").start();
new Thread(classRoom::leave, "学生A").start();
new Thread(classRoom::leave, "学生B").start();
new Thread(classRoom::leave, "学生C").start();
new Thread(classRoom::leave, "学生D").start();
new Thread(classRoom::leave, "学生E").start();
new Thread(classRoom::leave, "学生F").start();
}
}
因为值班学生调用了await方法,而普通学生调用了countDown方法,普通学生的线程不会阻塞,在计数器变为0之前,值班学生一直在等待,等待计数器为0,值班学生才开始做他的事情
循环栅栏CyclicBarrier
CyclicBarrier 的构造方法第一个参数是目标障碍数,才会执行cyclicBarrier.await()之后的语句,可以将CyclicBarrier 理解为加1操作
信号灯 Semaphore
在acquire()调用之前,没有调用acquire方法的线程都将处于阻塞状态,构造方法的数字表示只有3个许可,当acqurie的次数超过3个许可时,其他的线程都将等待,等待release方法调用,空出来一个许可,其他车才可停进去(也就是其他线程才会执行)
import java.util.concurrent.Semaphore;
/*
*6辆汽车停3个停车位
* */
public class ThreadDemo {
public static void main(String[] args)
{ //3个停车位
Semaphore semaphore =new Semaphore(3);
for(int i=1;i<=6;i++)
{
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"停进了停车场");
Thread.sleep(5000);
semaphore.release();
System.out.println(Thread.currentThread().getName()+"开出了停车场");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"汽车"+String.valueOf(i)).start();
}
}
}
内存可见性
volatile 关键字
线程在运行时,假设两个线程会共用一个共享资源,cpu会给每个线程分配独立的内存空间,线程的内存空间会对主存的资源进行拷贝,线程对其修改后,才会去合并主存,这时候,如果某个线程修改了值,但是另外一个线程在其合并前也拷贝了主存的值,就会两个不一样。虽然可以用synchronized加锁,但是加锁效率会很低,这时候volatile关键字,就类似实时刷新主存,线程之间的内存同步主存,做到线程之间的内存是可见的。
读写锁 ReentrantReadWriteLock
常见锁的概念
悲观锁:每次操作都要上锁解锁,好处是能解决并发中的各种问题,缺点是不支持并发操作,效率低
乐观锁:支持并发操作,要加一个版本号,每次线程提交事务都要检查版本号,看提交时的版本号是否和读取时的版本号一致,不一致说明在此之前有其他线程提交了事务,就不能提交事务了
表锁:对数据库里面某张表的某条记录进行操作时,对整张表进行上锁,其他人就无法对这张表进行操作了
行锁:对数据库里面的某张表的某行记录进行操作时,对该行记录进行上锁,其他人无法对该行记录进行操作,但是对于其他行记录仍然可以正常操作
读锁:共享锁,会发生死锁
写锁:独占锁,会发生死锁
读写锁:一个资源可以被多个读线程访问,或者可以被一个写线程访问,但是不能同时存在读写线程,读写互斥,读读共享
没加读写锁时Demo
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
public void put(String key,Object value) {
try {
System.out.println(Thread.currentThread().getName()+"正在写操作,key:"+key);
map.put(key,value);
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"写完了,key:"+key);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public Object get(String key)
{ Object result = null;
try {
System.out.println(Thread.currentThread().getName()+"正在读操作,key:"+key);
result = map.get(key);
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"读完了,key:"+key);
} catch (InterruptedException e) {
e.printStackTrace();
}
return result;
}
};
public class ThreadDemo {
public static void main(String[] args)
{
MyCache cache = new MyCache();
for(int i=0;i<=6;i++)
{ final int num = i;
new Thread(()->{
cache.put(num+"",num+"");
},"WriteThread"+ i).start();
}
for(int i=0;i<=6;i++)
{
final int num = i;
new Thread(()->{
cache.get(num+"");
},"ReadThread"+i).start();
}
}
}
可以看到还没写完就开始读了,这样肯定会读不到数据的.
加入读写锁后的Demo
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
private final ReentrantReadWriteLock ReadWriteLock = new ReentrantReadWriteLock();
public void put(String key,Object value) {
//加入写锁
ReadWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"正在写操作,key:"+key);
map.put(key,value);
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"写完了,key:"+key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
ReadWriteLock.writeLock().unlock();
}
}
public Object get(String key)
{ Object result = null;
//加入读锁
ReadWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"正在读操作,key:"+key);
result = map.get(key);
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"读完了,key:"+key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
ReadWriteLock.readLock().unlock();
}
return result;
}
};
public class ThreadDemo {
public static void main(String[] args)
{
MyCache cache = new MyCache();
for(int i=0;i<=6;i++)
{ final int num = i;
new Thread(()->{
cache.put(num+"",num+"");
},"WriteThread"+ i).start();
}
for(int i=0;i<=6;i++)
{
final int num = i;
new Thread(()->{
cache.get(num+"");
},"ReadThread"+i).start();
}
}
}
读写锁的演变过程
第一、无锁 多线程抢占资源 乱
第二、添加锁 使用synchronized和ReentrantLock 都是独占的 包括读读、读写、写写
第三、读写锁 ReetrantReadWriteLock 读读可以共享,提升性能,同时多人进行操作,但是读写,写写是独占的
读写锁的缺点
(1)造成锁饥饿,一直读,没有写操作
(2)读时候,不能写,只有读,完成之后,才可以写,写操作可以读
读写锁的降级
将写入锁降级为读锁
jdk8说明了锁的降级过程:
1)获取写锁
2)获取读锁
3)释放写锁
4)释放读锁
提高了数据的可见性,读锁不能升级为写锁
增加删除修改 —写操作
查询 —读操作
public class ThreadDemo {
public static void main(String[] args)
{
//可重入读写锁的对象
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//写锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//读锁
//锁降级
//获取写锁
writeLock.lock();
System.out.println("获取写锁....");
//获取写锁
readLock.lock();
System.out.println("获取读锁....");
//释放写锁
writeLock.unlock();
System.out.println("释放写锁....");
//释放读锁
readLock.unlock();
System.out.println("释放读锁....");
}
}
阻塞队列 BlockingQueue
先复习一下队列和栈的特点:
队列:先进先出
栈:后进先出
概述
阻塞队列,首先它是一个队列,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;
阻塞队列特点
1)当队列是空的,从队列中获取元素的操作将会被阻塞
2)当队列是满的,从队列中添加元素的操作将会被阻塞
3)试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
4)试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者已经完全清空,使队列变得空闲起来并后续新增
多线程领域:阻塞,指的是在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
什么时候需要BlockingQueue?
某些场景下,我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,类似生产线上一样,仓库满了就不能生产东西了,仓库为空的时候就不能提货(生产者和消费者模型)
阻塞队列应用:模拟生产者消费者模型Demo
(10个生产者,10个消费者)
/*
*阻塞队列的应用:
* 生产者消费者模型
*/
//生产车间,生产者调用生产方法,而消费者调用消费方法,生产者和消费者都要往这个工厂里面进行一系列操作
class Factory{
//最多能容纳10个产品存放
ArrayBlockingQueue<Object> ObjectFactory = new ArrayBlockingQueue<>(10);
public void produce()
{
try {
String product = Thread.currentThread().getName()+"生产的产品";
System.out.println(Thread.currentThread().getName()+"生产完成,等待把产品放到了仓库...");
TimeUnit.SECONDS.sleep(5);
ObjectFactory.put(product);
System.out.println(Thread.currentThread().getName()+"把产品放到了仓库...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void consumer() {
try{
System.out.println(Thread.currentThread().getName()+"等待获取可以消费的产品....");
String product = (String) ObjectFactory.take();
TimeUnit.SECONDS.sleep(6);
System.out.println(Thread.currentThread().getName()+"获取到消费的产品:"+product);}
catch (Exception e)
{
e.printStackTrace();
}
}
}
public class ThreadDemo {
public static void main(String[] args)
{
Factory factory = new Factory();
for(int i=0;i<=10;i++)
{
new Thread(()->{
while(true)
{
factory.produce();
}
},"生产者"+i).start();
}
for(int i=0;i<=10;i++)
{
new Thread(()->{
while(true)
{
factory.consumer();
}
},"消费者"+i).start();
}
}
}
阻塞队列的分类:
1)ArrayBlockingQueue,基于定长数组的阻塞队列,常用
2)DelayQueue ,使用优先级队列实现的延迟无界阻塞队列
3)LinkedBlockingDeque,由链表组成的双向阻塞队列
4)LinkedBlockingQueue,链表组成的无界阻塞队列,常用
5)PriorityBlockingQueue,使用优先级排序的无界阻塞队列
6)SynchronousQueue,不存储元素的阻塞队列,也即单个元素的队列
线程池
概述
一种线程使用模式,线程过多会带来调度开销,进而影响缓存局部性和整体性能,而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务,这避免了在处理短时间任务时创建和销毁线程的代价,线程池不仅能够保证内核的充分利用,还能过分调度
优势
线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程将排队等候,等其他线程执行完毕,再从队列中取出任务来
主要特点
降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行
提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
架构
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类
使用方式
一池n个线程:
Executors.newFixedThreadPool(n);
特征:
1)线程池中的线程处于一定的量,可以很好的控制线程的并发量
2)线程可以重复被使用,在显示关闭之前,都将一直存在
3)超出一定量的线程被提交时候需要在队列中等待
一个任务一个任务执行,一池一线程
Executors.newSingleThreadExecutor();
线程池根据需求创建线程,可扩容
Executors.newCachedThreadPool();
底层原理
上面三个方式底层都是new一个ThreadPoolExecutor完成线程池的创建
线程池的7个参数
int corePoolSize:线程池中常驻或者核心的线程数量
int maximumPoolSize:线程池中最大的线程数量
long keepAliveTime:不是常驻的线程的存活时间,搭配TimeUnit单位
TimeUnit unit:不是常驻的线程存活时间的单位
BlockingQueue workQueue:阻塞队列,超过了最大线程数量,就会发到阻塞队列
ThreadFactory threadFactory:线程工程,用于创建线程
RejectExecutionHandler handler:拒绝策略
线程池底层工作流程
1)在执行execute方法时才会新创建线程,在new线程池的时候不会创建
2)先用常驻线程,如果常驻不够,则会进入阻塞队列中等待,如果阻塞队列满了,再新建线程,如果到达线程池所设定的容纳的最大线程数,则会调用拒绝策略