一:Lock锁 (重点)
通过查看JDK文档 得知 Lock只是一个接口
它的两个核心方法是 lock 和unlock
Lock锁的 三个实现类
而最常用的 reentrantlock底层 公平锁 非公平锁
什么是公平锁?什么是非公平锁?
公平锁:线程安排得很公平,像队列一样,先来后到的顺序执行
非公平锁:线程安排是交由CPU调度的,可以理解为 插队!默认使用非公平锁
举例:如果有两个线程: 一个执行时间为3秒 ,另一个执行时间为 3小时,那么肯定需要让3秒的线程先执行,这时候就是 非公平锁的用途了~
传统的出现线程安全的代码:
package com.csnz;
public class LockDemo1 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i<50;i++){
ticket.sale();
}
}
},"线程一").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i<50;i++){
ticket.sale();
}
}
},"线程二").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i<50;i++){
ticket.sale();
}
}
},"线程三").start();
}
}
class Ticket{
private int ticket= 50;
public void sale(){
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"卖出第"+ticket--+"张票,剩余"+ticket+"张");
}
}
}
使用synchronized关键字修饰的代码(初级 解决线程安全问题方案)
使用Lock锁替换synchronized关键字的代码 (高级解决 线程安全问题的方案)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo2 {
public static void main(String[] args) {
Ticket2 ticket2 = new Ticket2();
new Thread( ()-> {
for (int i = 0;i<50;i++){
ticket2.sale();
}
},"线程一").start();
new Thread( ()-> {
for (int i = 0;i<50;i++){
ticket2.sale();
}
},"线程二").start();
new Thread( ()-> {
for (int i = 0;i<50;i++){
ticket2.sale();
}
},"线程三").start();
}
}
class Ticket2{
private int ticket= 50;
Lock lock = new ReentrantLock();// 1、新创建一个锁对象
public void sale(){
lock.lock(); // 给方法上锁
try {
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"卖出第"+ticket--+"张票,剩余"+ticket+"张");
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock(); //给方法解锁
}
}
}
Synchronized和Lock 的区别 (面试题)
1、synchronized 是内置的Java关键字,而 Lock 是一个Java类
2、synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
3、synchronized 会自动释放锁,而 Lock 必须要手动释放锁!如果不释放锁,会造成 死锁
4、synchronized 中如果线程1获得锁但是阻塞的话,线程2就必须永远的等待;但是Lock锁就不一定会一直等,tryLock()
5、synchronized 是可重入锁,不可以中断,非公平锁;但是Lock是可重入锁,可以 判断锁,默认非公平(但是可以自己设置成公平锁)
6、synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码!
二:生产者 消费者 问题
看似没问题的代码(初级)
//生产者 消费者 问题
public class ProducerConsumerIssues {
public static void main(String[] args) {
data data = new data();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
data.increment();
}
},"A").start();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
data.reduce();
}
},"B").start();
}
}
class data{
private int num = 0;
public synchronized void increment(){
//如果num 等于0 则 自增 否则 进入等待
if(num!=0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" : "+num++);
notifyAll();
}
public synchronized void reduce(){
//如果num != 0 则 自减 否则进入等待
if(num==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" : "+num --);
notifyAll();
}
}
运行结果:
程序停止打印,但是没结束(死锁出现)
问题存在:现在是两个线程,如果是四个线程呢?造成虚假唤醒
查看Object的源码
官网的解决方案:将if换成while 解决了虚化唤醒的问题,并且解决了死锁问题!
正确完整的生产者 消费者 代码实现
//生产者 消费者 问题
public class ProducerConsumerIssues {
public static void main(String[] args) {
data data = new data();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
data.increment();
}
},"A").start();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
data.reduce();
}
},"B").start();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
data.increment();
}
},"C").start();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
data.reduce();
}
},"D").start();
}
}
class data{
private int num = 0;
public synchronized void increment(){
//如果num 等于0 则 自增 否则 进入等待
while(num!=0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" : "+num++);
notifyAll();
}
public synchronized void reduce(){
//如果num != 0 则 自减 否则进入等待
while(num==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" : "+num --);
notifyAll();
}
}
作者发现,其实使用if-else判断也可以解决虚假唤醒问题,但是会造成线程死锁现象出现!
所以说还是用官方的方案:while循环判断~
JUC版本的生产者 消费者
使用 await()替换wait()方法
使用singleAll()替换 notifyAll()方法
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//JUC版本 生产者 消费者
public class JUCVersionProducerConsumerIssues {
public static void main(String[] args) {
dataJUC data = new dataJUC();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
data.increment();
}
},"A").start();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
data.reduce();
}
},"B").start();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
data.increment();
}
},"C").start();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
data.reduce();
}
},"D").start();
}
}
class dataJUC{
private int num = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//condition.await(); 等待
//condition.signalAll(); 唤醒
public void increment(){
lock.lock();
//try里面业务代码
try {
//如果num 等于0 则 自增 否则 进入等待
if(num!=0){
try {
condition.await();//等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
System.out.println(Thread.currentThread().getName() + " : " + num++);
condition.signalAll();//唤醒
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock(); //解锁
}
}
public void reduce(){
lock.lock(); //上锁
try {
//如果num != 0 则 自减 否则进入等待
if(num==0){
try {
condition.await(); //等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
System.out.println(Thread.currentThread().getName() + " : " + num--);
condition.signalAll(); //唤醒全部
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();//解锁
}
}
}
运行结果:
Condition 精准的通知和唤醒线程
解法:设置多个同步监视器,每一个去监视一个资源
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class JUCVersionPlus {
public static void main(String[] args) {
dataPlus dataPlus = new dataPlus();
new Thread(()->{
for (int i = 0; i < 10; i++) {
dataPlus.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
dataPlus.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
dataPlus.printC();
}
},"C").start();
}
}
/**
* @Des 资源类
* @Author 潮汕奴仔
* @Date 2021/8/12/0012 15:07
**/
class dataPlus{
private Lock lock = new ReentrantLock(); //锁对象
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
private int num = 1;
public void printA(){
lock.lock();
try {
//业务代码
while(num!=1){
conditionA.await();
}
System.out.println(Thread.currentThread().getName()+" -> 打印A");
num = 2;
conditionB.signal();//指定B 唤醒
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
//业务代码
while(num!=2){
conditionB.await();
}
System.out.println(Thread.currentThread().getName()+" -> 打印B");
num = 3;
conditionC.signal(); //唤醒 C
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
//业务代码
while(num!=3){
conditionC.await();
}
System.out.println(Thread.currentThread().getName()+" -> 打印C");
num = 1;
conditionA.signal();//唤醒A
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
三:八锁问题
关于锁的8个问题
No:1
1、标准情况下,两个线程 是先打印哪个?
2、如果将发送信息方法延迟2秒呢?哪个先执行?
上代码~
情况一:
public class demo1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendMsg();
},"A").start();
new Thread(()->{
phone.call();
},"A").start();
}
}
//资源类
class Phone{
public synchronized void sendMsg(){
System.out.println("发送信息");
}
public synchronized void call(){
System.out.println("打电话");
}
}
执行结果:
情况二:
修改发送信息方法,让他先睡两秒
public synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(2);//睡两秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送信息");
}
执行结果:
结论:
Synchronized 锁住的对象 是方法的调用者、这里即是phone对象。
两个方法用的是同一把锁,谁先拿到谁执行!
No:2
1、增加一个普通方法 main线程中调用同步方法和普通方法,发现普通方法先执行
2、两个资源对象,两个同步方法,发现没有延迟的同步方法先执行
上代码:
情况一:
public class demo2 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(()->{
phone.sendMsg();
},"A").start();
new Thread(()->{
phone.open();
},"A").start();
}
}
//资源类
class Phone2{
/*
Synchronized 锁住的对象 是方法的调用者、这里即是phone对象
两个方法用的是同一把锁,谁先拿到谁执行!
*/
public synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(2);//睡两秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送信息");
}
public synchronized void call(){
System.out.println("打电话");
}
//增加一个普通方法
public void open(){
System.out.println("开机...");
}
}
执行结果:
情况二:
执行结果:
结论:
两个对象用的两把锁,互不影响 ,所以没有延迟的会先执行
No3:
1、增加两个静态的同步方法,发现发短信先执行
2、两个对象,增加两个静态同步方法,发现发短信先执行
上代码:
情况一:
import java.util.concurrent.TimeUnit;
public class demo3 {
public static void main(String[] args) {
Phone3 phone = new Phone3();
new Thread(()->{
phone.sendMsg();
},"A").start();
new Thread(()->{
phone.call();
},"A").start();
}
}
//资源类 Phone3唯一的一个Class对象
class Phone3{
/*
Synchronized 锁住的对象 是方法的调用者、这里即是phone对象
static 静态方法 类一加载就有了,锁的Class对象(Phone3.Class)
两个方法用的是同一把锁,谁先拿到谁执行!
*/
public static synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(2);//睡两秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送信息");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
执行结果:
情况二:
执行结果:
结论:
同步方法只要是被static修饰的,也就是说 只要是静态的同步方法 他的锁就是唯一的Class对象,优先权直接看main中的顺序
No4:
1、一个静态的同步方法:一个普通的同步方法,一个对象,发现 先打印普通方法(无延迟的那个)
2、一个静态的同步方法,一个普通的同步方法,两个对象,发现还是先打印普通方法(无延迟的那个)
上代码:
情况一:
import java.util.concurrent.TimeUnit;
public class demo4 {
public static void main(String[] args) {
Phone4 phone = new Phone4();
new Thread(()->{
phone.sendMsg();
},"A").start();
new Thread(()->{
phone.call();
},"A").start();
}
}
//资源类 Phone3唯一的一个Class对象
class Phone4{
//静态的同步方法
public static synchronized void sendMsg(){
try {
TimeUnit.SECONDS.sleep(2);//睡两秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送信息");
}
//普通的同步方法
public synchronized void call(){
System.out.println("打电话");
}
}
执行结果:
情况二:
执行结果:
结论:
静态的同步方法和普通的同步方法因为 锁 不是同一个,所以他们互不干涉,普通方法他没有延迟,肯定就先执行了。
8锁结论:
静态的方法->锁为唯一的一个Class对象(执行顺序看main先后)
普通的同步方法看 调用者,同一个调用者 就是同一把锁,执行顺序看main先后
不同调用者,就是不同锁,执行顺序肯定是没有延迟的线程先执行
四:集合类安全问题
List类
拿ArrayList来说:看如下代码
我们同时创建多个线程添加试试~
结果不尽人意~
报了个 ConcurrentModificationException 异常
害,不知道什么东西~翻译翻译
原来是并发修改异常!!!从此,我们得知 ArrayList在并发下是不安全的
那么如何解决这一安全问题呢?解决方案有三种
1、使用Vector类
为什么Vector类是安全的呢?
客官~ 底层了解一下 ~
由于使用synchronized关键字修饰,所以他是线程安全的 ~~
那么我们顺便看看ArrayList底层是如何实现add的叭~
原来如此~ 那么为什么ArrayList不使用Synchronized修饰呢?难道是太老了,还没用这个方法???
我们来对比Vector和ArrayList的年纪
咦~ 看来不是忘记加的,是故意这么使用的。。。
2、使用Collections类的synchronizedList()方法
3、使用CopyOnWriteArrayList
解释一波 CopyOnWrite :俗称 写入时复制 简称 COW
它是计算机程序设计领域的一种优化策略
多个线程调用同一个对象时,在写入的时候前 ,先复制一份,添加完 ,再返回赋值(覆盖之前)
那么CopyOnWriteArrayList 比 Vector 好在哪里呢?
上面我们看了CopyOnWriteArrayList 底层实现使用了 Lock锁,而Vector底层使用的是 synchronized
论性能,Lock锁比 synchronized高~
Set类
在多线程下 使用HashSet 添加 同样会出现 并发修改异常 java.util.ConcurrentModificationException
public class SetTest {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
}).start();
}
}
}
解决方案有两种
1、使用 Collections.synchronizedSet()方法
2、使用 CopyOnWriteArraySet<>() 方法
顺便谈谈HashSet的底层实现
点进源码一看,好家伙!
这不是直接new了一个 hashMap 嫲~
再看看HashSet的add方法
本质还是 map的put方法 ,那么这个present是啥东西呢?
Map类
谈谈 HashMap
依旧老套路 上代码
public class MapTest {
public static void main(String[] args) {
Map<String,Object> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
果不其然,还是报了 并发修改异常
解决方案有两种
1、使用 Collections.synchronizedMap()方法
2、使用 ConcurrentHashMap() 方法
另外 HashMap 需知参数
五:Callable
我们来看看JDK官方文档
说明:Callable是一个可以有返回值、可以抛出异常,方法是call() 的函数式接口
上代码
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new MyThread());
new Thread(futureTask).start();
System.out.println(futureTask.get()); //打印 Callable的返回结果
}
}
class MyThread implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("将Callable交给FutureTask 再通过futureTask调用Thread");
return "潮汕奴仔";
}
}
执行结果
我们试一下启动多个线程
发现结果 只输出了一次 因为缓存的关系!
六:常用的辅助类
1、CountDownLatch
上代码
//减法计数器
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//总数 为 8
CountDownLatch countDownLatch = new CountDownLatch(8);
for (int i = 0; i < 8; i++) {
new Thread(()->{
System.out.println("同学: "+Thread.currentThread().getName()+"走人了");
countDownLatch.countDown(); //数量减一
},String.valueOf(i)).start();
}
countDownLatch.await();//等待计数器归零,然后程序向下执行
System.out.println("结束,可以锁门走人~");
}
}
原理
countDownLatch.coutDown(); //数量-1
countDownLatch.await(); //等待计数器归零,然后程序向下执行
每次有线程调用 countDown() 方法时,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续执行下面流程代码
2、CyclicBarrier
上代码
public class CyclicBarrierDemo {
public static void main(String[] args) {
//表白潮汕奴仔的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(6,()->System.out.println("思念潮汕奴仔"));
//思念6天后 表白潮汕奴仔
for (int i = 0; i < 6; i++) {
final int temp = i;
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"思念潮汕奴仔的第"+temp+"天");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
执行结果;
3、Semaphore (并发中常用)
信号量· 限流场景下多见
题目:场景:停车位抢夺大战
例如:潮汕奴仔家里有三个停车位,但是这天他有6个许多好友来,好友们都开车过来,那么只有三个停车位该如何抢夺呢?
上代码
public class SemaphoreDemo {
public static void main(String[] args) {
//可以 想成线程数量 (限流场景)
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(()->{
//acquire()得到
//release()释放
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到车位...");
TimeUnit.SECONDS.sleep(2);//停2秒 再开走
System.out.println(Thread.currentThread().getName()+"正在离开...");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();// 释放
}
},String.valueOf(i)+"号车 ").start();
}
}
}
原理:
semaphore.acquire(); 获得,假如已经满了,等待,直到被释放为止
semaphore.release(); 释放,会将当前的信号量释放 +1,然后唤醒其他等待的线程
作用:多个共享资源互斥的使用,并发限流,控制最大的线程数!
七:读写锁 ReadWriteLock
上代码
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/*
独占锁(写锁) :一次只能被一个线程占有
共享锁(读锁) :多个线程可以同时占有
ReadWriteLock
读与读 可以共存,读和写 无法共存,写和写 无法共存
*/
public class ReadWriteLockTest {
public static void main(String[] args) {
myCache cache = new myCache();
//写入缓存
for (int i = 1; i < 6; i++) {
String temp = String.valueOf(i);
new Thread(()->{
cache.put(temp,temp);
},String.valueOf(i)).start();
}
//读取缓存
for (int i = 1; i < 6; i++) {
String temp = String.valueOf(i);
new Thread(()->{
cache.get(temp);
}).start();
}
}
}
class myCache{
private Map<String,Object> cache = new HashMap<>();//存放缓存的map
private ReadWriteLock lock = new ReentrantReadWriteLock();
//写入缓存的时候,只让一个线程操作
public void put(String name,Object value){
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"正在写入数据...");
cache.put(name,value);
System.out.println(Thread.currentThread().getName()+"写入成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
//读取缓存时,可以多个线程同时读取
public void get(String name){
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"正在读取数据...");
cache.get(name);
System.out.println(Thread.currentThread().getName()+"读取成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
}
运行结果
八:阻塞队列 BlockingQueue
初始结构目录
一般在什么情况下会使用阻塞队列:并发编程、线程池
阻塞队列的特点:
写入:如果队列满了,就必须阻塞等待
取出:如果队列为空,必须阻塞等待生产
关于四组API
1、抛出异常
/**
* @Des 抛出异常
* @Date 2021/8/14/0014 21:49
* @Param []
* @Return void
*/
public static void test(){
//参数为 队列容量
ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(4);
System.out.println(queue.add("潮"));//true
System.out.println(queue.add("汕"));//true
System.out.println(queue.add("奴"));//true
System.out.println(queue.add("仔"));//true
//java.lang.IllegalStateException: Queue full
//System.out.println(queue.add("再添加就超过容量了,要报错了!"));
System.out.println(queue.remove());//潮
System.out.println(queue.remove());//汕
System.out.println(queue.remove());//奴
System.out.println(queue.remove());//仔
//再删除就 java.util.NoSuchElementException
//System.out.println(queue.remove());//
}
2、有返回值
/**
* @Des 有返回值,没有异常
* @Date 2021/8/14/0014 21:57
* @Param []
* @Return void
*/
public static void test2(){
ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.offer(1));//true
System.out.println(queue.offer(2));//true
System.out.println(queue.offer(3));//true
System.out.println(queue.offer(4));//false 不会报错
System.out.println(queue.poll());//1
System.out.println(queue.poll());//2
System.out.println(queue.poll());//3
System.out.println(queue.poll());//null 不会报错
}
3、阻塞等待
/**
* @Des 等待一直阻塞
* @Date 2021/8/14/0014 22:11
* @Param []
* @Return void
*/
public static void test3() throws InterruptedException {
ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(3);
queue.put(1);
queue.put(2);
queue.put(3);
// queue.put("添加不进去了,会造成阻塞,程序不会停止...");
System.out.println(queue.take());//1
System.out.println(queue.take());//2
System.out.println(queue.take());//3
}
4、超时等待
/**
* @Des 超过指定时长后 停止等待
* @Date 2021/8/14/0014 22:26
* @Param []
* @Return void
*/
public static void test4() throws InterruptedException {
ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.offer(1));//true
System.out.println(queue.offer(2));//true
System.out.println(queue.offer(3));//true
System.out.println(queue.offer(4,2, TimeUnit.SECONDS));//过两秒后打印 false
System.out.println(queue.poll());//1
System.out.println(queue.poll());//2
System.out.println(queue.poll());//3
System.out.println(queue.poll(2,TimeUnit.SECONDS));//过两秒后打印 null
}
九:同步队列
上一个点是阻塞队列,那么下面我们来讲一下他的一个实现类~ 同步队列 SynchronousQueue
我们可以看一下他的方法~
代码练习
同步队列:特点:没有容量,进去一个元素,必须等待取出来之后,才能再往里面放元素!
和其他的BlockingQueue 不一样,SynchronousQueue不存储元素
put了一个元素后,必须从里面先take 将元素取出来,否则无法 put进去
public class SQueue {
public static void main(String[] args) {
//同步队列
BlockingQueue queue = new SynchronousQueue();
//试图一次性存入三个数据
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" 存入 1");
queue.put(1);
System.out.println(Thread.currentThread().getName()+" 存入 2");
queue.put(2);
System.out.println(Thread.currentThread().getName()+" 存入 3");
queue.put(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
//试图一次性取出三个数据
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+" 取出 "+queue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+" 取出 "+queue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+" 取出 "+queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"B").start();
}
}
执行结果:
注意 代码中 存入数据时 打印语句应该在put之前执行,不然put后就直接停止了等待take
会造成打印输出 先take 再put‘的情况
十:线程池
线程池具有:三大方法、七大参数、四种拒绝策略
池化技术
程序的运行,本质:占用系统的资源!因为系统资源有限,所以我们需要优化资源~ 诞生 池化技术
线程池、连接池、内存池、对象池、等等,有关线程的创建和销毁都是十分浪费资源的
池化技术:事先准备好一些资源,有人要用,就来池中拿,用完之后还回池中
线程池的好处:
1、降低资源的消耗
2、提高响应的速度
3、方便管理。
4、线程复用,可以控制最大并发数量,集中管理线程
三大方法
单个线程的 线程池、固定的线程池大小、(容量)可伸缩的线程池
Executors.newSingleThreadExecutor();//单个线程的 线程池
Executors.newFixedThreadPool(6); //创建一个固定的线程池大小
Executors.newCachedThreadPool(); //创建 容量可伸缩的线程池
单个线程的 线程池
public class Demo1 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程的 线程池
//ExecutorService threadPool = Executors.newFixedThreadPool(6); //创建一个固定的线程池大小
//ExecutorService threadPool = Executors.newCachedThreadPool(); //创建一个 (容量)可伸缩的线程池
try {
for (int i = 0; i < 10; i++) {
//使用线程池后,使用线程池来创建线程
threadPool.execute(()->System.out.println(Thread.currentThread().getName()+"被调用..."));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
执行结果:
固定大小(容量)的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(6);
执行结果:
线程重复被调用
(容量)可伸缩的线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
执行结果:
七大的对象
我们想探究七大对象,首先需要探究三大方法的底层原理
我们可以清楚的看到三大方法调用的底层 都是去new了一个ThreadPoolExecutor类,只是参数不同罢了。
点进去,解开神秘的面纱~
接下来手动创建一个线程池来探究七大对象
如图所示,银行柜台一般开放2个核心窗口(雷打不动的开放),另外三个窗口只有人流量大才会开放。
候客区就相当于 阻塞队列 如果上面窗口满了就需要在这里等待
场景一:业务数量少于 最大线程数量(即最多5个人访问银行,银行最多有5个窗口,3个等候区位置)
public class MyExecutor { //Executors 工具类,三大方法 七大对象
public static void main(String[] args) {
// 自定义线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2, //核心线程数量
5, //最大线程数量
10, //除了核心线程 其他线程的保持存活时间
TimeUnit.SECONDS, //线程的保持存活时间单位 秒
new LinkedBlockingQueue<>(3), //阻塞队列 可以给定容量
Executors.defaultThreadFactory(), //创建线程的工厂 默认就好,一般不会变化
new ThreadPoolExecutor.AbortPolicy()); //拒绝策略
try {
for (int i = 1; i <= 5; i++) {
//使用线程池来创建线程
threadPoolExecutor.execute(()-> System.out.println(Thread.currentThread().getName()+"正在处理业务"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//线程池用完,程序结束,关闭线程池
threadPoolExecutor.shutdown();
}
}
}
如果是这种情况的话,只会启用核心线程,即2个线程。
场景二:业务数量大于 最大线程数量(即最多6个人访问银行,银行最多有5个窗口,3个等候区位置)
因为有六个人,而现在2个在核心窗口,3个在等候室,还剩一个没位置,所以需要开启一个窗口
场景三:业务数量大于 最大线程数量(即最多7个人访问银行,银行最多有5个窗口,3个等候区位置)
道理和上面一样,等候区放不下了就会开启一个新窗口
场景四:业务数量大于 最大线程数量(即最多8个人访问银行,银行最多有5个窗口,3个等候区位置)
如今五个窗口都开放了
场景五:业务数量大于 最大线程数量(即已经超过8个人访问银行,银行最多有5个窗口,3个等候区位置)
此时银行柜台5个窗口打开了,等候区3个位置也占满了,剩下一个人没地方去,就会按照你所选择的拒绝策略 输出对应信息
我们可以更换拒绝策略来 实现多种场景
四种拒绝策略
像我们上面那个demo
选择默认的拒绝策略 AbortPolicy()
1、new ThreadPoolExecutor.AbortPolicy()); //银行满了,还有人进来,直接抛出异常,不处理此人
2、new ThreadPoolExecutor.CallerRunsPolicy()); //哪里来的回哪去
3、new ThreadPoolExecutor.DiscardPolicy()); //队列满了,丢掉任务,不会抛出异常
注意任务不会被执行
4、new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试和最早的线程竞争,不会抛出异常
可以看出和上面的区别,下面第九个线程还是被执行了
线程池小结:
问题:池的容量大小应该如何设置?(也就是调优)
需考虑两个因素:
1、CPU密集型,几核,就写几个,这样子做可以保证CPU的效率最高
2、IO 密集型 , 因为IO操作是十分占用资源的,所以 这个容量设置的具体值 应该大于 你程序中IO的线程,最好是线程容量 > 耗IO资源线程的两倍
十一:JMM
概念:Java内存模型,是一种抽象概念,并非真实存在。它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(比如类的成员变量、静态属性等等)的访问方式
关于JMM的一些同步的约定:
- 1、线程解锁前,必须把共享变量 立刻 刷新(同步)回 主内存
- 2、线程加锁前,必须读取主内存中的最新值 到 工作内存中
- 3、加锁和解锁 必须是同一把锁
JMM的3大特性:
1、可见性
一个线程将自己工作内存内共享变量的最新值刷新回主内存,其他线程能收到主内存中共享变量值发生改变的通知,并能看到最新的改变,这种性质即JMM中的可见性。Java里的可见性是通过底层的Lock前缀的指令以及缓存一致性协议(比如MESI)实现的
2、原子性
一个线程在进行某个操作过程中,不可被打断,不可被分割,它要么执行成功,要么执行失败,不可处于一种中间状态。要求原子性是为了保证数据的完整性。Java里的原子性是通过CAS实现的
3、有序性
计算机执行程序时,为了提高执行性能,编译期和处理器常常会对指令进行重排序,一般分以下3种: 源代码–>编译期优化的重排–>指令并行的重排–>内存系统的重排–>最终执行的指令 。
Java里的有序性是通过内存屏障实现的
注:指令重排:
可以保证串行语义一致,但是没有义务保证多线程间的语义一致,对于提高CPU处理性能是十分重要的
哪些指令不能重排: Happen-Before 规则
-
程序顺序原则:一个线程内保证语义的串行性
-
volatile规则:volatile 变量的写,先发生于读,这保证了volatile变量的可见性
-
锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
-
传递性:A先于B,B先于C,那么A必然先于C
-
线程的start()方法先于它的每一个动作
-
线程的所有操作先于线程的终结(Thread.join())
-
线程的中断(interrupt())先于被中断线程的代码
-
对象的构造函数执行,结束先于finalize() 方法
如何避免指令重排:使用内存屏障
什么是内存屏障:
内存屏障,也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。
大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。
为什么要使用内存屏障:
内存可见性问题,主要是高速缓存与内存的一致性问题。一个处理器上的线程修改了某数据,而在另一处理器上的线程可能仍然使用着该数据在专用cache中的老值,这就是可见性出了问题。解决办法是令该数据为volatile属性,或者读该数据之前执行内存屏障
内存屏障的两个作用:
1、阻止屏障两侧的指令发生重排序;
2、强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效
关于 线程 工作内存和主内存的8种操作
十二:Volatile
如图所示,因为线程A不知道 主内存中的值已经被修改过了,需要需要volatile登场!
概念:是Java 虚拟机提供 轻量级的同步机制
volatile的特性
1、保证可见性
2、不能保证原子性
3、由于CPU的内存屏障,可以避免指令重排
为何不能保证原子性
原子性释义:不可分隔
线程A在执行任务的时候,不能被打扰,也不能被分隔,要么同时成功,要么同时失败
分析一下add()方法 发现num++并不是一个原子性操作
如果要保证原子性的话 在add方法上添加lock或者synchronized就能实现原子性了
但是如果不使用lock和synchronized呢?使用JUC的原子类~
避免指令重排 图示:
十三:单例模式
饿汉式
程序一开始就 创建好了,等待调用(无论需不需要使用 都会创建)
懒汉式
//懒汉式 单例 用时才创建
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName()+"进入了懒汉式");
}
private static Lazy instance;
public static Lazy getInstance(){
if(instance==null)
instance = new Lazy();//如果没有实例 再创建实例
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
Lazy.getInstance();
}).start();
}
}
}
上述的代码 在单线程下是没问题的,但是在多线程下会出现多个实例
进阶 -> 使用双重检测锁模式的懒汉式 简称 DCL懒汉式
//双重检测锁模式的懒汉式 简称 DCL懒汉式
public static Lazy getInstance(){
if(instance==null){
synchronized (Lazy.class){
if(instance==null)
instance = new Lazy();//如果没有实例 再创建实例
}
}
return instance;
}
极端情况出现:指令重排现象
完整的DCL懒汉式 代码
//完整的DCL懒汉式单例 : 用时才创建
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName()+"进入了懒汉式");
}
private volatile static Lazy instance;
//双重检测锁模式的懒汉式 简称 DCL懒汉式
public static Lazy getInstance(){
if(instance==null){ //第一次判断
synchronized (Lazy.class){ // 加锁
if(instance==null) // 第二次判断
//new 不是一个原子性操作,可能发生指令重排 所以需要 给实例加上volatile
instance = new Lazy();//如果没有实例 再创建实例
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
Lazy.getInstance();
}).start();
}
}
}
为什么枚举 能避免 单例模式 被破坏?
创建一个枚举类
//枚举类
public enum MyEnum {
instance;
public static MyEnum getInstance(){
return instance;
}
}
通过getInstance获取枚举的实例
MyEnum instance =MyEnum.getInstance();
MyEnum instance2 =MyEnum.getInstance();
System.out.println(instance+"\t"+instance2);
结果:实例相同
通过放射获取枚举的实例
Constructor<MyEnum> constructor = MyEnum.class.getDeclaredConstructor(null);
constructor.setAccessible(true);//暴力破解 阻止java.lang.IllegalAccessException
MyEnum myEnum1 = constructor.newInstance();
MyEnum myEnum2 = constructor.newInstance();
System.out.println(myEnum1+"\t"+myEnum2);
结果报错:java.lang.NoSuchMethodException: com.csnz.single.MyEnum.<init>()
查看class文件发现 他里面是一个空构造器 (假象)
通过查看jdk官方文档得知 枚举类唯一的构造器 protected Enum(String name,int ordinal)
于是乎 我们使用这个构造器进行 反射创建
Constructor<MyEnum> constructor = MyEnum.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);//暴力破解 阻止java.lang.IllegalAccessException
MyEnum myEnum1 = constructor.newInstance();
MyEnum myEnum2 = constructor.newInstance();
System.out.println(myEnum1+"\t"+myEnum2);
结果:java.lang.IllegalArgumentException: Cannot reflectively create enum objects
查看源码 发现确实无法使用反射创建枚举类的实例
十四:CAS探究
什么是CAS:
全称Compare-and-Swap,是指比较和交换,比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的,则执行交换操作,如果不是就一直循环。
底层原理:Unsafe类
探究 getAndIncrement()方法
进入unsafe类探究~
同时,这个方法还有一个知识点:自旋锁
CAS缺点:
1、循环会耗时
2、一次性只能保证一个共享变量的原子性
3、引发ABA问题
CAS 引发 ABA问题
什么是ABA问题?
ABA场景
运维小弟 借用 小红,用完没通知小猿,小猿一直不知道它的女朋友借给别人用的悲惨故事…
解决ABA问题:就是可以理解为,当运维小弟借用小猿的女朋友后,小猿可以得知女朋友被借用了,直接和小红分手!而不是傻傻的被蒙在鼓里
如何解决ABA问题呢?引出 原子引用问题 => SQL中对应的解决思想:乐观锁
原子引用:解决ABA问题;思想:乐观锁;原理:带版本号的原子操作
上代码:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
//ABA问题
public class ABADemo {
public static void main(String[] args) {
//初始值是66 版本号是1 注意:这里泛型 一般在开发中都是引用对象
AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(66,1);
new Thread(()->{
int stamp = reference.getStamp();//获得版本号
System.out.println(Thread.currentThread().getName()+" 获得的版本号为:"+stamp);
try {
TimeUnit.SECONDS.sleep(2); // 睡两秒,让线程B 拿到和A 一样的版本号
}catch (InterruptedException e){
e.printStackTrace();
}
boolean flag = reference.compareAndSet(66, 88,reference.getStamp(),reference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+" 将66 换成 88 => "+flag);
System.out.println(Thread.currentThread().getName()+" 更新获得的版本号为:"+reference.getStamp());
//再还回来
boolean flag2= reference.compareAndSet(88,66,reference.getStamp(),reference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+" 将88 换成 66 => "+flag2);
System.out.println(Thread.currentThread().getName()+" 更新获得的版本号为:"+reference.getStamp());
},"A").start();
//和乐观锁的原理相同
new Thread(()->{
int stamp = reference.getStamp();//获取版本号
System.out.println(Thread.currentThread().getName()+" 获得的版本号为:"+stamp);
try {
TimeUnit.SECONDS.sleep(3); // 睡两秒, 拿到和A 一样的版本号
}catch (InterruptedException e){
e.printStackTrace();
}
boolean flag = reference.compareAndSet(66, 99, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName()+" 将66 换成 99 => "+flag);
System.out.println(Thread.currentThread().getName()+" 更新获得的版本号为:"+reference.getStamp());
},"B").start();
}
}
原子引用代码剖析:
十五:各种锁
公平锁、非公平锁 上面已经有解释了~
可重入锁:也称递归锁
synchronized版本可重入锁
//可重入锁
public class Demo1 {
public static void main(String[] args) {
Person person = new Person();
new Thread(()->person.openDoor(),"A").start();//A 直到出了副卧室才解锁
new Thread(()->person.openDoor(),"B").start();//B 直到出了副卧室才解锁
}
}
class Person{
//进入家门
public synchronized void openDoor(){
System.out.println(Thread.currentThread().getName()+" 开门...");
intoRoom();
intoRoom2();
}
//进入主卧室
public synchronized void intoRoom(){
System.out.println(Thread.currentThread().getName()+" 进入了主卧室...");
}
//进入副卧室
public synchronized void intoRoom2(){
System.out.println(Thread.currentThread().getName()+" 进入了副卧室...");
}
}
Lock版本可重入锁
public class Demo2 {
public static void main(String[] args) {
People people = new People();
new Thread(()->people.openDoor(),"A").start();//A 直到出了副卧室才解锁
new Thread(()->people.openDoor(),"B").start();//B 直到出了副卧室才解锁
}
}
class People{
Lock lock = new ReentrantLock();
//进入家门
public void openDoor(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+" 开门...");
intoRoom();
intoRoom2();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//进入主卧室
public void intoRoom(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+" 进入了主卧室...");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//进入副卧室
public void intoRoom2(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+" 进入了副卧室...");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
自旋锁
例如上面的自增操作 就是一个自旋锁
下面开始我们的测试代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
//自旋锁
public class SpinlockDemo {
AtomicReference<Thread> reference = new AtomicReference<Thread>();
//加锁 需要循环
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"开始加锁...");
//自旋锁
while(!reference.compareAndSet(null,thread)){
}
}
//解锁 不需要循环
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"开始解锁...");
reference.compareAndSet(thread,null);
}
public static void main(String[] args) throws InterruptedException {
SpinlockDemo lock = new SpinlockDemo();
new Thread(()->{
lock.myLock();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
},"A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
lock.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
},"B").start();
}
}
死锁:
关于死锁的理解:
不同的线程分别占用对方需要的同步资源不放弃
都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
死锁现象说明:
出现死锁后,不会出现异常,不会 出现提示,只是所有的线程都处于阻塞状态,无法继续
我们使用同步时,要避免使用死锁
造成死锁四要素
1、互斥: 某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
2、占有且等待: 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
3、不可抢占: 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
4、循环等待: 存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
死锁必定要满足这四个,而不是满足了这四个就一定死锁,也许还要加上其它条件才会死锁。
来一个造成死锁的demo
//死锁
public class DeadLockDemo {
public static void main(String[] args) {
String LockA = "LockA";
String LockB = "LockB";
new Thread(()->new MyThread(LockA,LockB).run(),"A").start();
new Thread(()->new MyThread(LockB,LockA).run(),"B").start();
}
}
class MyThread implements Runnable{
String LockA = "LockA";
String LockB = "LockB";
public MyThread(String lockA, String lockB) {
LockA = lockA;
LockB = lockB;
}
@Override
public void run() {
synchronized (LockA){
System.out.println(Thread.currentThread().getName()+"获取了"+LockA+" 试图获取"+LockB);
synchronized (LockB){
System.out.println(Thread.currentThread().getName()+"获取了"+LockB+" 试图获取"+LockA);
}
}
}
}
执行结果:
如何解决死锁问题呢?
一般在工作中排查死锁可以通过日志 和 堆栈信息进行查看
1、使用jps -l
定位进程号
2、使用jstack 进程号
查看死锁问题