多线程与JUC并发编程
1.多线程
1.线程简介
任务,进程,线程,多线程
1.1任务与多任务
1.2 多线程
多线程是一种同时执行多个任务的能力,它允许程序同时执行多个部分,从而提高了程序的性能和效率。在多线程编程中,程序可以同时处理多个任务,而不是按照顺序逐个执行。这样可以更充分地利用计算机的资源,提高程序的响应速度和处理能力。
1.3进程
- 程序(Program): 程序是一组指令的集合,用于执行特定的任务。它通常以源代码的形式存在,可以被编译或解释成可执行的文件。程序是静态的,它只是存储在磁盘上的一段代码,直到被加载到内存中才开始执行。
- 进程(Process): 进程是正在执行的程序的实例。当程序被加载到内存并开始执行时,就会创建一个进程。每个进程都有自己的地址空间、内存、文件句柄等资源,相互之间独立运行,彼此不会影响。进程是操作系统进行资源分配和调度的基本单位。
- 线程(Thread): 线程是进程内的一个独立执行单元,是程序执行流的最小单位。一个进程可以包含多个线程,它们共享相同的地址空间和其他资源,但每个线程都有自己的栈空间和执行状态。线程之间可以并发执行,彼此之间可以共享数据和通信。
总的来说,程序是静态的代码,进程是程序的执行实例,而线程是进程内部的独立执行单元。多个线程可以在同一进程内并发执行,共享进程的资源。
2.线程创建(重点)
Thead,Runnable,Callble
三种创建方式
1.继承Thread类
2.实现Runnable接口
3.实现Callable接口
1.继承Thread类(重点)
package com.qingfeng.thread;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
/**
* @description TODO
* @author: qingfeng
* @date 2024-05-04 10:47
*/
//1.继承Thread类实现多线程
public class Thread01 extends Thread{
private String url;
private String fileName;
public Thread01(String url,String name){
this.fileName=name;
this.url=url;
}
//2.重载run方法,里面写这个线程需要执行的逻辑
@Override
public void run() {
WebDownloader downloader = new WebDownloader();
downloader.downloader(this.url,this.fileName);
System.out.println(this.fileName+"名称的文件下载完毕");
}
public static void main(String[] args) {
//3.在main类中,创建这个线程对象
Thread01 t1 = new Thread01("https://www.kuangstudy.com/assert/course/c1/14.jpg","test01.jpg");
Thread01 t2 = new Thread01("https://www.kuangstudy.com/assert/course/c1/14.jpg","test02.jpg");
Thread01 t3 = new Thread01("https://www.kuangstudy.com/assert/course/c1/14.jpg","test03.jpg");
//4.线程对象调用start方法来执行该线程
t1.start();
t2.start();
t3.start();
}
}
//下载器
class WebDownloader{
//下载方法
public void downloader(String url,String fileName){
try {
FileUtils.copyURLToFile(new URL(url),new File(fileName));
} catch (IOException e) {
e.printStackTrace();
System.out.println("io异常,下载器出现问题");
}
}
}
2.继承Runnable接口(重点)
ackage com.qingfeng.thread;
/**
* @description TODO
* @author: qingfeng
* @date 2024-05-04 11:13
*/
public class Thread02 implements Runnable{
private String url;
private String fileName;
public Thread02(String url,String name){
this.fileName=name;
this.url=url;
}
@Override
public void run() {
WebDownloader downloader = new WebDownloader();
downloader.downloader(url,fileName);
System.out.println(this.fileName+"名称的文件下载完毕");
}
public static void main(String[] args) {
Thread02 t1 = new Thread02("https://www.kuangstudy.com/assert/course/c1/14.jpg","test01.jpg");
Thread02 t2 = new Thread02("https://www.kuangstudy.com/assert/course/c1/14.jpg","test02.jpg");
Thread02 t3 = new Thread02("https://www.kuangstudy.com/assert/course/c1/14.jpg","test03.jpg");
new Thread(t1).start();
new Thread(t2).start();
new Thread(t3).start();
}
}
/**
* 多线程操作一个对象
* 以买火车票为例子
*/
public class Thread03 implements Runnable{
private int ticketNum = 10;
@Override
public void run() {
while (true){
if (ticketNum<0){
break;
}
try{
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNum--+"张票");
}
}
public static void main(String[] args) {
Thread03 tickThread = new Thread03();
new Thread(tickThread,"学生").start();
new Thread(tickThread,"老师").start();
new Thread(tickThread,"黄牛").start();
new Thread(tickThread,"黄牛").start();
}
}
/**
* 结果如下,这种时候就发生资源抢占的问题
* 学生拿到了第8张票
* 黄牛拿到了第9张票
* 黄牛拿到了第7张票
* 老师拿到了第10张票
* 学生拿到了第6张票
* 黄牛拿到了第4张票
* 老师拿到了第6张票
* 黄牛拿到了第5张票
* 老师拿到了第3张票
* 黄牛拿到了第1张票
* 黄牛拿到了第2张票
* 学生拿到了第3张票
* 黄牛拿到了第0张票
* 学生拿到了第0张票
* 老师拿到了第-1张票
* 黄牛拿到了第0张票
*/
/**
* @description
* @author: qingfeng
* @date 2024-05-04 11:43
*/
public class Race implements Runnable{
private String winner;
@Override
public void run() {
boolean flag;
for (int i=1;i<=100;i++){
if(Thread.currentThread().getName().equals("兔子")&&i%20==0){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果比赛结束则结束循环
flag=gameOver(i);
if(flag){
break;
}
System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
}
}
//判断比赛是否结束
private boolean gameOver(int steps){
if(winner!=null){
//如果已经有胜利者,后续将不再判断
return true;
}else {
if(steps>=100){
winner=Thread.currentThread().getName();
System.out.println("比赛结束,胜利者是:"+winner);
return true;
}else {
return false;
}
}
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"兔子").start();
new Thread(race,"乌龟").start();
}
}
3.实现callable接口(了解)
实现Callable接口需要重写call方法,call需要返回值(装箱)
实现Runnable接口重写run方法,无返回值(void)
/**
* callable的好处
* 1.可以定义返回值
* 2.可以抛出异常
*/
public class TestCallable implements Callable {
private String url;
private String name;
public TestCallable(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public Boolean call() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("文件名为"+name+"的文件下载完毕!");
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable("https://www.kuangstudy.com/assert/course/c1/14.jpg", "test1.jpg");
TestCallable t2 = new TestCallable("https://www.kuangstudy.com/assert/course/c1/12.jpg", "test2.jpg");
TestCallable t3 = new TestCallable("https://www.kuangstudy.com/assert/course/c1/11.jpg", "test3.jpg");
//创建执行服务:
ExecutorService server = Executors.newFixedThreadPool(2);
//提交执行
Future<Boolean> result1 = server.submit(t1);
Future<Boolean> result2 = server.submit(t2);
Future<Boolean> result3 = server.submit(t3);
//获取结果
boolean rs1 = result1.get();
boolean rs2 = result2.get();
boolean rs3 = result3.get();
System.out.println(rs1);
System.out.println(rs2);
System.out.println(rs3);
//关闭服务
server.shutdownNow();
}
}
//下载器
class WebDownloader{
//下载方法
public void downloader(String url, String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("io异常,downloader出现问题");
}
}
}
3.静态代理
/**
* @description 静态代理模式总结:
* 真实对象和代理对象都要实现同一个接口
* 代理对象要代理真实角色
*
* 好处:
* 代理对象可以做很多,真实对象做不了的事情
* 真实对象可以专注做自己的事情
* @author: qingfeng
* @date 2024-05-05 10:18
*/
public class StaticProxy {
public static void main(String[] args) {
//其实多线程也是运用了静态代理模式
new Thread(()-> System.out.println("我爱你!")).start();
new WeddingCompany(new You()).happyMerry();
}
}
interface Merry{
void happyMerry();
}
//真实角色
class You implements Merry{
@Override
public void happyMerry() {
System.out.println("qingfeng结婚了,好高兴啊");
}
}
//代理角色,婚庆公司,包办你的结婚过程
class WeddingCompany implements Merry{
private Merry target;
WeddingCompany(Merry target){
this.target=target;
}
@Override
public void happyMerry() {
//代理对象可以在之前和之后实现一些其他方法
before();
target.happyMerry(); //被代理的对象的方法
after();
}
private void before(){
System.out.println("结婚前准备筹划");
}
private void after(){
System.out.println("结婚后,受尾款");
}
}
4.Lambda表达式
5.线程状态
1.停止线程的方法
public class TestStop implements Runnable{
//1.线程暂停标志位
private boolean flag = true;
@Override
public void run() {
int i=0;
while (flag){
System.out.println("run...Thread"+i++);
}
}
//对外提供一个线程停止的方法
public void stop(){
this.flag=false;
}
public static void main(String[] args) {
TestStop stop = new TestStop();
new Thread(stop).start();
for (int i = 0; i < 100; i++) {
System.out.println("main Running:"+i);
if(i==88){
stop.stop();
System.out.println("新线程结束!");
}
}
}
}
2.线程休眠
Sleep可以放大问题的不安全性
//倒计时例子
public static void main(String[] args) {
Date startTime = new Date(System.currentTimeMillis());
while (true){
try {
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime = new Date(System.currentTimeMillis());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
3.线程礼让yield
4.Join线程合并
5.观察线程状态
/**
* @description 测试线程状态
* @author: qingfeng
* @date 2024-05-05 11:48
*/
public class testState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread end");
});
System.out.println(thread.getState()); //new
thread.start();
System.out.println(thread.getState()); //RUNNABLE
//新线程不结束就,主线程一直打印状态
while (thread.getState()!=Thread.State.TERMINATED){
Thread.sleep(100);
System.out.println(thread.getState());//TIME_WAITING
}
//TERMINATED
//线程死亡后无法再start,如果再调用start方法会报错
// thread.start();
}
}
6.线程优先级
public class TestPriority {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"->"+Thread.currentThread().getPriority());
Thread t1 = new Thread(new MyPriority());
Thread t2 = new Thread(new MyPriority());
Thread t3 = new Thread(new MyPriority());
Thread t4 = new Thread(new MyPriority());
Thread t5 = new Thread(new MyPriority());
//默认线程优先度
t1.start();
//最小线程优先度
t2.setPriority(Thread.MIN_PRIORITY);
t2.start();
//最大线程优先度
t3.setPriority(Thread.MAX_PRIORITY);
t3.start();
//设置大于线程优先度的值,会报错
// t4.setPriority(11);
// t4.start();
//
// //设置小于线程最小优先度,会报错
// t5.setPriority(0);
// t5.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"->"+Thread.currentThread().getPriority());
}
}
守护线程
setDaemon(true)
gc垃圾回收线程是守护线程
守护线程在程序停止的时候才停止
6.线程同步(重点)
每个Object对象都拥有一把锁
锁造成的问题 保证了安全损失了性能
三大不安全例子
1.不安全买票
/**
* @description 不安全买票
* @author: qingfeng
* @date 2024-05-05 18:52
*/
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket ticket = new BuyTicket();
new Thread(ticket,"小明").start();
new Thread(ticket,"林琳").start();
new Thread(ticket,"黄牛").start();
/**
* 小明买到了第3张票
* 黄牛买到了第3张票
* 林琳买到了第2张票
* 黄牛买到了第1张票
* 票卖完了
* 林琳买到了第1张票
* 小明买到了第1张票
*/
}
}
class BuyTicket implements Runnable{
//票
private int ticketNums = 10;
//外部停止标志
boolean flag = true;
@Override
public void run() {
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void buy() throws InterruptedException {
if(ticketNums<=0){
System.out.println("票卖完了");
flag=false;
return;
}
Thread.sleep(200);
System.out.println(Thread.currentThread().getName()+"买到了第"+ticketNums--+"张票");
}
}
2.不安全取钱
/**
* @description 不安全取钱
* @author: qingfeng
* @date 2024-05-05 19:11
*/
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "结婚基金");
new Bank(account,50,"未婚夫").start();
new Bank(account,100,"未婚妻").start();
/**
* 未婚夫余额为:-50
* 未婚妻余额为:-50
* 未婚夫手里的钱为::50
* 未婚妻手里的钱为::100
*/
}
}
//银行账号
class Account{
int money;
String name;
public Account(int money,String name){
this.money=money;
this.name=name;
}
}
//银行模拟取钱
class Bank extends Thread{
Account account;
int drawingMoney;//取钱数额
int nowMoney;//手里拿到的钱
public Bank(Account account,int drawingMoney,String name){
super(name);
this.account=account;
this.drawingMoney=drawingMoney;
}
//取钱操作
@Override
public void run() {
//钱不够时退出
if(account.money-drawingMoney<0){
System.out.println(Thread.currentThread().getName()+"钱不够,取钱失败");
return;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果钱够
//手里的钱
nowMoney+=drawingMoney;
//余额
account.money-=drawingMoney;
System.out.println(Thread.currentThread().getName()+"余额为:"+account.money);
System.out.println(Thread.currentThread().getName()+"手里的钱为::"+nowMoney);
}
}
3.不安全链表
/**
* @description 不安全链表
* @author: qingfeng
* @date 2024-05-05 19:27
*/
public class UnsafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list =new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
//放大问题
// Thread.sleep(600);
System.out.println(list.size()); //995
}
}
2.线程安全化
弊端:锁的粒度较大
一般只读的方法不需要锁,修改的方法才需要上锁
同步块
- 同步方法锁对象本身(this)
- 同步块可以锁任何对象
不安全案例的安全化
1.买票
/**
* @description 不安全买票+解决
* @author: qingfeng
* @date 2024-05-05 18:52
*/
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket ticket = new BuyTicket();
new Thread(ticket,"小明").start();
new Thread(ticket,"林琳").start();
new Thread(ticket,"黄牛").start();
}
}
class BuyTicket implements Runnable{
//票
private int ticketNums = 10;
//外部停止标志
boolean flag = true;
@Override
public void run() {
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//重构,加synchronized上锁
/**
* 小明买到了第10张票
* 小明买到了第9张票
* 小明买到了第8张票
* 小明买到了第7张票
* 小明买到了第6张票
* 小明买到了第5张票
* 小明买到了第4张票
* 小明买到了第3张票
* 小明买到了第2张票
* 小明买到了第1张票
* 票卖完了
* 票卖完了
* 票卖完了
*/
private synchronized void buy() throws InterruptedException {
if(ticketNums<=0){
System.out.println("票卖完了");
flag=false;
return;
}
Thread.sleep(200);
System.out.println(Thread.currentThread().getName()+"买到了第"+ticketNums--+"张票");
}
}
2.银行取钱
/**
* @description 不安全取钱
* @author: qingfeng
* @date 2024-05-05 19:11
*/
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "结婚基金");
new Bank(account,50,"未婚夫").start();
new Bank(account,100,"未婚妻").start();
/**
* 未婚夫余额为:-50
* 未婚妻余额为:-50
* 未婚夫手里的钱为::50
* 未婚妻手里的钱为::100
*/
}
}
//银行账号
class Account{
int money;
String name;
public Account(int money,String name){
this.money=money;
this.name=name;
}
}
//银行模拟取钱
class Bank extends Thread{
Account account;
int drawingMoney;//取钱数额
int nowMoney;//手里拿到的钱
public Bank(Account account,int drawingMoney,String name){
super(name);
this.account=account;
this.drawingMoney=drawingMoney;
}
//取钱操作
@Override
public void run() {
//使用synchronized(obj)锁,里面的obj是需要增删改的对象
/**
* 未婚夫余额为:50
* 未婚夫手里的钱为::50
* 未婚妻钱不够,取钱失败
*/
synchronized (account){
//钱不够时退出
if(account.money-drawingMoney<0){
System.out.println(Thread.currentThread().getName()+"钱不够,取钱失败");
return;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果钱够
//手里的钱
nowMoney+=drawingMoney;
//余额
account.money-=drawingMoney;
System.out.println(Thread.currentThread().getName()+"余额为:"+account.money);
System.out.println(Thread.currentThread().getName()+"手里的钱为::"+nowMoney);
}
}
}
3.不安全链表
public class UnsafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list =new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
//放大问题
Thread.sleep(600);
System.out.println(list.size()); //995
}
}
JUC集合
死锁
/**
* @description 死锁测试
* @author: qingfeng
* @date 2024-05-05 20:19
*/
public class DeadLock {
public static void main(String[] args) {
Makeup t1 = new Makeup(0, "小红");
Makeup t2 = new Makeup(1, "小兰");
t1.start();
t2.start();
}
}
//镜子资源
class Mirror{}
//口红资源
class LipStick{}
//化妆
class Makeup extends Thread{
//使用静态类,保证镜子资源和口号资源只有一份
static Mirror mirror = new Mirror();
static LipStick lipStick = new LipStick();
int choice; //选择
String name;//使用化妆品的人
public Makeup(int choice,String name){
this.choice=choice;
this.name=name;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void makeup() throws InterruptedException {
/**
* 小红获得了镜子锁
* 小兰获得了口红锁
*
* 程序一直停留在这两个状态,进入了死锁
*/
if(choice==0){
synchronized (mirror){//一开始获得镜子锁
System.out.println(this.name+"获得了镜子锁");
Thread.sleep(1000);
synchronized (lipStick){//一秒后获得口红锁
System.out.println(this.name+"获得了口红锁");
}
}
}else {
synchronized (lipStick){//一开始获得口红锁
System.out.println(this.name+"获得了口红锁");
Thread.sleep(2000);
synchronized (mirror){//一秒后获得镜子锁
System.out.println(this.name+"获得了镜子锁");
}
}
}
}
}
Lock(锁)
- synchonized是隐式的锁 lock是显示的锁
ReentrantLock
可重入锁 实现了Lock接口
public class TestLock {
public static void main(String[] args) {
TestLock2 lock2 = new TestLock2();
new Thread(lock2).start();
new Thread(lock2).start();
new Thread(lock2).start();
}
}
class TestLock2 implements Runnable{
int tickNums =10;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try{
Thread.sleep(200);
lock.lock();
if(tickNums>0){
System.out.println(Thread.currentThread().getName()+"买到"+tickNums--);
}else {
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
synchronized和lock对比
线程通信
1.管程法
/**
* @description 测试:生产者消费者模型->利用缓冲区解决:管程法
* 需要的资源:生产者,消费者,产品,缓冲区
* @author: qingfeng
* @date 2024-05-05 21:46
*/
public class TestPC {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
Productor productor = new Productor(synContainer);
Consumer consumer = new Consumer(synContainer);
new Thread(productor,"生产者").start();
new Thread(consumer,"消费者").start();
}
}
class Productor implements Runnable{
SynContainer container;
public Productor(SynContainer synContainer){
container=synContainer;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
container.push(new Chicken(i));
System.out.println(Thread.currentThread().getName()+"生产了第"+i+"只鸡");
}
}
}
class Consumer implements Runnable{
SynContainer container;
public Consumer(SynContainer synContainer){
container=synContainer;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName()+"消费了第"+ container.pop().id+"只鸡");
}
}
}
//产品:鸡
class Chicken{
int id; //编号
public Chicken(int id){
this.id=id;
}
}
//缓冲区
class SynContainer{
//初始化容器大小
Chicken[] chickens =new Chicken[10];
//容器计数器
int count=0;
//容器添加产品方法:生产者生产鸡
public synchronized void push(Chicken chicken){
//如果容器满了
if(count==chickens.length){
//生产者进入等待状态
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果容器没满,则往容器放入产品
chickens[count]=chicken;
count++;
//通知消费者消费
this.notifyAll();
}
//从容器中取出产品:消费者消费产品
public synchronized Chicken pop(){
//如果容器没有产品
if(count<=0){
//消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果容器里面有产品,消费者消费
count--;
Chicken chicken =chickens[count];
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
//通知生产者生产
this.notifyAll();
return chicken;
}
}
2.信号灯法
/**
* @description 信号灯法,标志位解决
* 生产者:演员,消费者:观众,产品:节目
* @author: qingfeng
* @date 2024-05-05 22:24
*/
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
//生产者--》演员
class Player extends Thread{
private TV tv;
public Player(TV tv){
this.tv=tv;
}
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
if(i%2==0){
try {
tv.play("斗破苍穹"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
try {
tv.play("blibli广告"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//消费者--》观众
class Watcher extends Thread{
private TV tv;
public Watcher(TV tv){
this.tv=tv;
}
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
try {
tv.watch();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//产品———》节目
class TV{
//演员表演时,观众等待 T
//观众观看时,演员等待 F
String voice;//节目名称
boolean flag=true;
//演员表演,生产
public synchronized void play(String voice) throws InterruptedException {
if(!flag){
//演员抢到cpu时查看如果是观众观看状态,则等待观众观看结束
this.wait();
}
System.out.println("演员表演了"+voice);
//通知观众来观看
this.notifyAll();
this.voice=voice;
this.flag=!flag;
}
//观众观看节目,消费
public synchronized void watch() throws InterruptedException {
if(flag){
//观众抢到cpu时查看如果是演员表演状态,则等待演员表演结束
this.wait();
}
System.out.println("观众观看了"+voice);
//通知演员表演
this.notifyAll();
this.flag=!flag;
}
}
线程池
线程可以充分利用
- Runnable 使用 void execute 因为没有返回值
- Callable 使用submit 因为有返回值
public class TestPool {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.shutdownNow();
}
}
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
多线程总结
public class finishTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
new MyThread1().start();
new Thread(new MyThread2()).start();
ExecutorService service = Executors.newFixedThreadPool(1);
Future<Integer> submit = service.submit(new MyThread3());
Integer integer = submit.get();
System.out.println("Callable返回值为"+integer);
service.shutdownNow();
//Callable的另一种实现方式
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread3());
new Thread(futureTask).start();
Integer integer1 = futureTask.get();
System.out.println("Callable返回值为"+integer1);
}
}
//1.继承Thread类
class MyThread1 extends Thread{
@Override
public void run() {
System.out.println("继承Thread类");
}
}
//2.实现Runnable接口
class MyThread2 implements Runnable{
@Override
public void run() {
System.out.println("实现Runnable接口");
}
}
//3.实现Callable接口
class MyThread3 implements Callable{
@Override
public Integer call() throws Exception {
System.out.println("实现Callable接口");
return 1;
}
}
2.JUC并发编程
1.JUC脑图
1. 什么是JUC
源码 + 官方文档 面试高频问!
JUC就是java.util.concurrent
java.util 工具包、包、分类
业务:普通的线程代码 Thread
- Runnable 没有返回值、效率相比入 Callable 相对较低!
2、线程和进程
2.1 进程 / 线程是什么?
进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程可以利用进程所有拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
白话:
进程:一个程序,QQ.exe Music.exe 程序的集合;
一个进程往往可以包含多个线程,至少包含一个!
Q:Java默认有几个线程?
A: 2 个 mian、GC
线程:开了一个进程 Typora,写字,自动保存(线程负责的)
对于Java而言:Thread、Runnable、Callable
Java 真的可以开启线程吗? 开不了
2.2 并发 / 并行是什么?
并发不一定是同时的
并行一定是同时的
做并发编程之前,必须首先理解什么是并发,什么是并行。
并发和并行是两个非常容易混淆的概念。它们都可以表示两个或多个任务一起执行,但是偏重点有点不
同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。并发是逻辑上的同时发生
(simultaneous),而并行是物理上的同时发生。然而并行的偏重点在于”同时执行”。
严格意义上来说,并行的多个任务是真实的同时执行,而对于并发来说,这个过程只是交替的,一会运
行任务一,一会儿又运行任务二,系统会不停地在两者间切换。但对于外部观察者来说,即使多个任务是
串行并发的,也会造成是多个任务并行执行的错觉。
实际上,如果系统内只有一个CPU,而现在而使用多线程或者多线程任务,那么真实环境中这些任务不
可能真实并行的,毕竟一个CPU一次只能执行一条指令,这种情况下多线程或者多线程任务就是并发
的,而不是并行,操作系统会不停的切换任务。真正的并发也只能够出现在拥有多个CPU的系统中(多
核CPU)。
并发的动机:在计算能力恒定的情况下处理更多的任务, 就像我们的大脑, 计算能力相对恒定, 要在一天中
处理更多的问题, 我们就必须具备多任务的能力. 现实工作中有很多事情可能会中断你的当前任务, 处理这
种多任务的能力就是你的并发能力。
并行的动机:用更多的CPU核心更快的完成任务. 就像一个团队, 一个脑袋不够用了, 一个团队来一起处理
一个任务。
例子:
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 (不一定是
同时的)
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
2.3 线程的状态
public enum State {
//新生
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待
WAITING,
//超时等待
TIMED_WAITING,
//终止
TERMINATED;
}
阻塞和等待的对比
1、一句话概括
在java中,线程阻塞状态是线程本身不可计划的,而线程等待状态是线程本身计划之内的。2、相同点与不同点
相同点:
(1)都会暂停线程的执行。区别点:
(1)线程进入阻塞状态是被动的, 而线程进入等待状态是主动的。
阻塞状态的被动:线程在同步代码外,获取对象锁失败时,线程进入阻塞状态;何时获取对象锁失败不可知,即线程阻塞状态是线程本身不可计划的。
等待状态的主动:线程在同步代码内,等待其他线程操作时,线程接入等待状态;何时等待其他线程操作可知,即线程等待状态是线程本身计划之内的。
2.4 wait和sleep的区别
1、来自不同的类
这两个方法来自不同的类分别是,sleep来自Thread类,wait来自Object类。
sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
2、关于锁的释放
最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。sleep是线程被调用时,占着cpu去睡觉,其他线程不能占用cpu,os认为该线程正在工作,不会让出系统资源,wait是进入等待池等待,让出系统资源,其他线程可以占用cpu。
sleep(100L)是占用cpu,线程休眠100毫秒,其他进程不能再占用cpu资源,wait(100L)是进入等待池中等待,交出cpu等系统资源供其他进程使用,在这100毫秒中,该线程可以被其他线程notify,但不同的是其他在等待池中的线程不被notify不会出来,但这个线程在等待100毫秒后会自动进入就绪队列等待系统分配资源,换句话说,sleep(100)在100毫秒后肯定会运行,但wait在100毫秒后还有等待os调用分配资源,所以wait100的停止运行时间是不确定的,但至少是100毫秒。
就是说sleep有时间限制的就像闹钟一样到时候就叫了,而wait是无限期的除非用户主动notify。
3、使用范围不同
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
synchronized(x){
//或者wait()
x.notify()
4、是否需要捕获异常
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
3、Lock锁(重点)
3.1 传统的 synchronized
/**
* @description 企业中真正多线程开发,降低耦合度
* 线程就是一个单独资源类,不应该有其他操作
* 只包含:属性,操作属性的方法即可
* @author: qingfeng
* @date 2024-05-06 0:37
*/
public class SaleTicket01 {
public static void main(String[] args) {
//并发编程:多线程操作同一个资源类,把这个类扔进线程里面
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 20; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
ticket.sale();
}
},"C").start();
}
}
//资源,票
class Ticket{
//属性,票数
private int ticketNums=20;
//操作属性的方法,卖票
//synchronized
public synchronized void sale(){
if(ticketNums>0){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出了第"
+ticketNums--+"张票,剩余票数:"+ticketNums);
}
}
}
3.2 Lock接口
文档中建议使用try catch finally , unlock在finally中
ReentrantLock类
可重入锁
公平锁:十分公平:可以先来后到
非公平锁:十分不公平:可以插队 (ReentrantLock和synchronized默认)
public class SaleTicket02 {
public static void main(String[] args) {
//并发编程:多线程操作同一个资源类,把这个类扔进线程里面
Ticket1 ticket = new Ticket1();
new Thread(()->{
for (int i = 0; i < 20; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
ticket.sale();
}
},"C").start();
}
}
//资源,票
class Ticket1{
//属性,票数
private int ticketNums=20;
Lock lock = new ReentrantLock();
//操作属性的方法,卖票
//synchronized
public void sale(){
lock.lock(); //加锁
try {
if(ticketNums>0) {
System.out.println(Thread.currentThread().getName() + "卖出了第"
+ ticketNums-- + "张票,剩余票数:" + ticketNums);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock(); //解锁
}
}
}
3.3 Synchronized 和 Lock 区别
- Synchronized 内置的Java关键字, Lock 是一个Java类
- Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
- Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
- Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去(tryLock尝试获取锁);
- Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以自己设置);
- Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!
可重入锁是一种同步机制,通常用于多线程编程中,用于控制对共享资源的访问。"可重入"指的是同一个线程可以多次获取该锁,而不会发生死锁或阻塞自己。简单来说,当一个线程持有了该锁之后,即使在持有锁的代码段中再次请求该锁,也能够再次获取到,而不会被阻塞。这种机制确保了线程在执行临界区代码时不会被其他线程打断或互斥地执行同一个临界区代码,从而提高了程序的并发性和性能。
锁是什么,如何判断锁的是谁!
在Java中,锁是一种同步机制,用于控制多个线程对共享资源的访问。锁可以防止多个线程同时访问某个共享资源,从而避免竞争条件和数据不一致性问题。
Java提供了多种类型的锁,包括 synchronized 关键字、ReentrantLock 类等。这些锁都有一个共同的特点,即它们可以用来保护临界区代码,确保同一时刻只有一个线程可以执行该代码块。
判断锁的归属通常是根据锁对象来确定的。在使用 synchronized 关键字时,锁对象通常是被 synchronized 修饰的方法所属的对象,也可以是指定的对象。而在使用 ReentrantLock 类时,锁对象就是 ReentrantLock 实例本身。因此,可以通过分析代码中的锁对象来判断锁的归属。
例如,在 synchronized 方法中,锁的归属通常是该方法所属的对象。在使用 ReentrantLock 类时,通过创建 ReentrantLock 的实例来作为锁对象,锁的归属就是该实例。
4、生产者和消费者问题
面试的:单例模式、排序算法、生产者和消费者、死锁
线程间的通信 , 线程之间要协调和调度
4.1生产者和消费者 Synchronized 版
/**
* @description
* 线程之间的通信问题:生产者消费者问题! 等待唤醒,通知幻想
* * 线程交替执行 A B操作同一个资源
* * A num += 1
* * B num -= 1
* @author: qingfeng
* @date 2024-05-06 15:19
*/
public class testPC01 {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
//线程通信一般步骤:判断等待,业务,唤醒
class Data{
//资源
private int num=0;
//+1
public synchronized void increment() throws InterruptedException {
if(num!=0){
//判断等待
this.wait();
}
//业务操作
num++;
System.out.println(Thread.currentThread().getName()+"==>"+num);
//唤醒
notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
if(num==0){
//判断等待
wait();
}
//业务操作
num--;
System.out.println(Thread.currentThread().getName()+"==>"+num);
//唤醒
this.notifyAll();
}
}
4.2 虚假唤醒问题
如果这时候创建四个线程,两个生产两个消费,就会发生虚假唤醒问题
解决方法:if 改为 while 判断
Q:为什么用if会发生虚假唤醒问题?
A:拿两个加法线程A、B来说,比如A先执行,执行时调用了wait方法,那它会等待,此时会释放锁,那么线程B获得锁并且也会执行wait方法,两个加线程一起等待被唤醒。此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程,那么这俩加线程不会一起执行,其中A获取了锁并且加1,执行完毕之后B再执行。如果是if的话,那么A修改完num后,B不会再去判断num的值,直接会给num+1。如果是while的话,A执行完之后,B还会去判断num的值,因此就不会执行。
(重点) wait方法应该永远放在while循环中
4.3 JUC版生产者和消费者写法
/**
* @description
* 线程之间的通信问题:生产者消费者问题! 等待唤醒,通知幻想
* * 线程交替执行 A B操作同一个资源
* * A num += 1
* * B num -= 1
* 使用JUC的方法实现
* @author: qingfeng
* @date 2024-05-06 15:19
*/
public class testPC01 {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
//线程通信一般步骤:判断等待,业务,唤醒
class Data{
//资源
private int num=0;
private Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//+1
public void increment() throws InterruptedException {
lock.lock();
try {
while (num!=0){
//判断等待
condition.await();
}
//业务操作
num++;
System.out.println(Thread.currentThread().getName()+"==>"+num);
//唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//-1
public void decrement() throws InterruptedException {
lock.lock();
try {
while (num==0){
//判断等待
condition.await();
}
//业务操作
num--;
System.out.println(Thread.currentThread().getName()+"==>"+num);
//唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
此时线程是随机的状态,如何让他像A->B->C->D一样有序执行呢?
4.4 精确通知顺序访问
通过多个Condition来控制
/**
* @description
* 线程之间的通信问题:生产者消费者问题! 等待唤醒,通知幻想
* * 线程交替执行 A B操作同一个资源
* * A num += 1
* * B num -= 1
* 使用JUC的方法实现
* @author: qingfeng
* @date 2024-05-06 15:19
*/
public class testPC01 {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
//线程通信一般步骤:判断等待,业务,唤醒
class Data{
//资源
private int num=0;
private Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
Condition condition4 = lock.newCondition();
private int count=1;//count%4==1:A 2:B,3:C,0:D
//+1
public void increment() throws InterruptedException {
lock.lock();
try {
while (num!=0&&Thread.currentThread().getName().equals("A")){
//判断等待
condition1.await();
}
while (num!=0&&Thread.currentThread().getName().equals("C")){
//判断等待
condition3.await();
}
//业务操作
num++;
count++;
System.out.println(Thread.currentThread().getName()+"==>"+num);
//唤醒
if (Thread.currentThread().getName().equals("A")){
condition2.signal();
}
if (Thread.currentThread().getName().equals("C")){
condition4.signal();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//-1
public void decrement() throws InterruptedException {
lock.lock();
try {
while (num==0&&Thread.currentThread().getName().equals("B")){
//判断等待
condition2.await();
}
while (num==0&&Thread.currentThread().getName().equals("D")){
//判断等待
condition4.await();
}
//业务操作
num--;
count++;
System.out.println(Thread.currentThread().getName()+"==>"+num);
//唤醒
if (Thread.currentThread().getName().equals("B")){
condition3.signal();
}
if(Thread.currentThread().getName().equals("D")){
condition1.signal();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
5、8锁现象
如何判断锁的是谁!永远的知道什么锁,锁到底锁的是谁!
1、标准访问,请问先打印邮件还是短信?
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public synchronized void sendEmail() throws Exception{
System.out.println("sendEmail");
}
public synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
}
//sendEmail
//sendSMS
结论:被synchronized修饰的方法,锁的对象是方法的调用者
。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先执行。
2、邮件方法暂停4秒钟,请问先打印邮件还是短信?
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4); //add
System.out.println("sendEmail");
}
public synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
}
//sendEmail
//sendSMS
结论:被synchronized修饰的方法,锁的对象是方法的调用者
。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先执行
,第二个方法只有在第一个方法执行完释放锁之后才能执行。
3、新增一个普通方法hello()没有同步,请问先打印邮件还是hello?
/**
* 多线程的8锁
* 1、标准访问,请问先打印邮件还是短信?
* 2、邮件方法暂停4秒钟,请问先打印邮件还是短信?
* 3、新增一个普通方法hello()没有同步,请问先打印邮件还是hello?
*/
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone.hello();
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("sendEmail");
}
public synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
//不受锁的影响
public void hello(){
System.out.println("Hello");
}
}
//Hello
//sendEmail
//sendSMS
结论:新增的方法没有被synchronized修饰,不是同步方法,不受锁的影响,所以不需要等待。其他线程共用了一把锁,所以还需要等待。
4、两部手机、请问先打印邮件还是短信?
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
try {
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone2.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("sendEmail");
}
public synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
}
//sendSMS
//sendEmail
结论:被synchronized修饰的方法,锁的对象是方法的调用者。因为用了两个对象调用各自的方法,所以两个方法的调用者不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。
5、两个静态同步方法,同一部手机,请问先打印邮件还是短信?
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
new Thread(()->{
try {
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone1.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public static synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("sendEmail");
}
public static synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
}
//sendEmail
//sendSMS
结论:static锁的不是对象,锁的是Class
. 被synchronized和static修饰的方法,锁的对象是类的class对象。因为两个同步方法都被static修饰了,所以两个方法用的是同一个锁,后调用的方法需要等待先调用的方法。
6、两个静态同步方法,2部手机,请问先打印邮件还是短信?
package org.example.juc.lock8;
import java.util.concurrent.TimeUnit;
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
try {
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone2.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public static synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("sendEmail");
}
public static synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
}
//sendEmail
//sendSMS
结论:被synchronized和static修饰的方法,锁的对象是类的class对象。因为两个同步方法都被static修饰了,即便用了两个不同的对象调用方法,两个方法用的还是同一个锁,后调用的方法需要等待先调用的方法。
7、一个普通同步方法,一个静态同步方法,同一部手机,请问先打印邮件还是短信?
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
new Thread(()->{
try {
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone1.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public static synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("sendEmail");
}
public synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
}
//sendSMS
//sendEmail
结论:被synchronized和static修饰的方法,锁的对象是类的class对象。仅仅被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法锁的对象不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。
注意:class被锁了不会影响这个类的对象
8、一个普通同步方法,一个静态同步方法,2部手机,请问先打印邮件还是短信?
package org.example.juc.lock8;
import java.util.concurrent.TimeUnit;
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
try {
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
TimeUnit.MILLISECONDS.sleep(100);
new Thread(()->{
try {
phone2.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public static synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("sendEmail");
}
public synchronized void sendSMS() throws Exception{
System.out.println("sendSMS");
}
}
//sendSMS
//sendEmail
结论:被synchronized和static修饰的方法,锁的对象是类的class对象。仅仅被synchronized修饰的方法,锁的对象是方法的调用者。即便是用同一个对象调用两个方法,锁的对象也不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。
小结
new this 具体的一个手机
static class 唯一的一个模板
一个对象里面如果有多个synchronized方法,某个时刻内,只要一个线程去调用其中一个synchronized方法了,其他的线程都要等待,换句话说,在某个时刻内,只能有唯一一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronized方法
加个普通方法后发现和同步锁无关,换成两个对象后,不是同一把锁,情况立刻变化
都换成静态同步方法后,情况又变化了。所有的非静态的同步方法用的都是同一把锁----实例对象本身synchronized实现同步的基础:java中的每一个对象都可以作为锁
具体的表现为以下三种形式:
对于普通同步方法,锁的是当前实例对象
对于静态同步方法,锁的是当前的Class对象。
对于同步方法块,锁是synchronized括号里面的配置对象
当一个线程试图访问同步代码块时,他首先必须得到锁,退出或者是抛出异常时必须释放锁,也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可以是别的实例对象非非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以必须等待该实例对象已经获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
所有的静态同步方法用的也是同一把锁----类对象本身这两把锁的是两个不同的对象,所以静态的同步方法与非静态的同步方法之间是不会有竞争条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要他们用一个的是同一个类的实例对象。
6.集合类不安全
1.list并发访问不安全
*/
public class UnsafeList {
public static void main(String[] args) {
//ArrayList是不安全的集合,在并发环境下访问必定会报错:
// java.util.ConcurrentModificationException(并发修改异常)
// List<String> list = new ArrayList<>();
/**
* 解决方案
* 1. List<String> list = new Vector<>();
* 2. List<String> list = Collections.synchronizedList(new ArrayList<>());
* 3. List<String> list = new CopyOnWriteArrayList<>();
*/
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},i+"").start();
}
}
}
写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array
CopyOnWriteArrayList为什么并发安全且性能比Vector好
CopyOnWriteArrayList 读没加锁 Vector 增删改查都加锁了
我知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。
2.不安全Set
public class SetTest {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
}).start();
}
}
}
Exception in thread “Thread-4” java.util.ConcurrentModificationException
public class SetTest {
public static void main(String[] args) {
/**
* 解决方法
* 1. Collections 工具类
* Set<String> set = Collections.synchronizedSet(new HashSet<String>());
* 2.Set<String> set = new CopyOnWriteArraySet<>();
*/
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
}).start();
}
}
}
hashset底层是什么
hashset底层就是hashMap
,只用了HashMap的Key
add方法 就是map的put方法
3.不安全Map
Map<String,String> map = new HashMap<>();
// 等价于 容量 加载因子
Map<String,String> map = new HashMap<>(16,0.75);
// 工作中,常常会自己根据业务来写参数,提高效率
public class MapTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName().toString(),
UUID.randomUUID().toString().substring(0,5).toString());
System.out.println(map);
}).start();
}
}
}
并发环境下会报错java.util.ConcurrentModificationException
解决
public class MapTest {
public static void main(String[] args) {
/**
* 解决方法
* 1.Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
* 2.Map<String, String> map = new ConcurrentHashMap<>(); 名字不再是CopyOnWrite开头
*/
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName().toString(),
UUID.randomUUID().toString().substring(0,5).toString());
System.out.println(map);
}).start();
}
}
}
7、Callable ( 简单 )
多线程中,第3种获得多线程的方式,Callable。它与Runnable有什么区别呢?
- 有返回值
- 可以抛异常
- 方法不一样,一个是call,一个是run
泛型的参数类型等于方法的返回值类型
FutureTask是 Runnable接口的实现类,所以我们可以拿到FutureTask传入new Thread()中
在FutureTask类中,有两个构造方法,分别能与Callable和Runnable连接
所以通过Futuretask传入一个Callable就能创建实现Runnable接口的Futuretask对象
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//new Thread(new Runnable()).start();
//new Thread(new FutureTask<V>()).start();
//new Thread(new FutureTask<V>( Callable )).start();
MyThread thread = new MyThread();
FutureTask<Integer> integerFutureTask = new FutureTask<>(thread);
new Thread(integerFutureTask,"A").start();
new Thread(integerFutureTask,"B").start();
//返回的值
System.out.println(integerFutureTask.get());
/**
* A:callable
* 1024
*
* 只输入 A:callable,没有B: callable是因为结果会被缓存,所以只执行一次
*/
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+":callable");
return 1024;
}
}
8、常用辅助类(必会)
8.1、CountDownLatch
减法计数器,能保证所有线程执行完再继续执行
原理:
- CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞
- 其他线程调用CountDown方法会将计数器减1(调用CountDown方法的线程不会阻塞)
- 当计数器变为0时,await 方法阻塞的线程会被唤醒,继续执行
/**
* @description 技术器,用于前置条件必须执行完毕才向下执行的情况
* @author: qingfeng
* @date 2024-05-06 21:17
*/
public class TestCountDownLatch {
public static void main(String[] args) throws InterruptedException {
//设置总数是6,必须要执行任务的时候,在使用
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"get out");
countDownLatch.countDown();
},i+"").start();
}
countDownLatch.await();//等待计数器归零后再向下执行
System.out.println("close door");
}
}
8.2、CyclicBarrier
加法计数器 调用等待的个数到达目标数字后开启一个线程
翻译:CyclicBarrier 篱栅
作用:和上面的减法相反,这里是加法,好比集齐7个龙珠召唤神龙,或者人到齐了再开会!
public class TestCycliBarrier {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("集齐了7颗龙珠,可以召唤神龙了");
});
for (int i = 1; i <= 7; i++) {
final 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();
}
}
}
8.3、Semaphore
翻译:Semaphore 信号量;信号灯;信号
作用:抢车位
原理:
在信号量上我们定义两种操作:
acquire(获取)
当一个线程调用 acquire 操作时,他要么通过成功获取信号量(信号量-1)
要么一直等下去,直到有线程释放信号量,或超时
release (释放)
实际上会将信号量的值 + 1,然后唤醒等待的线程。
信号量主要用于两个目的:一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
public class TestSemaphore {
public static void main(String[] args) {
//停车场有三个车位,模拟多辆车抢占车位
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
},String.valueOf(i)).start();
}
/**
* 4抢到车位
* 0抢到车位
* 5抢到车位
* 5离开车位
* 0离开车位
* 3抢到车位
* 1抢到车位
* 4离开车位
* 2抢到车位
* 3离开车位
* 1离开车位
* 2离开车位
*/
}
}
9、读写锁
读的时候可以多个线程同时读
写的时候只能有一个线程写
/**
* @description 读写锁
* 独占锁(写锁):一次只能被一个线程占有
* 共享锁(读锁):多个线程可以同时占有
*
* 读-读 可以共存
* 读写 不可以共存
* 写写 不可以共存
* @author: qingfeng
* @date 2024-05-06 22:11
*/
public class TestReadWriteLock {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(()->{
myCache.set(String.valueOf(temp),temp+temp);
},String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(()->{
myCache.get(String.valueOf(temp));
},String.valueOf(i)).start();
}
}
}
class MyCache{
private volatile Map<String,Object> map =new HashMap<>();
//添加读写锁
private ReentrantReadWriteLock lock =new ReentrantReadWriteLock();
//存,写,多线程互斥地写入数据
public void set(String key,Object value){
//添加写锁
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入数据之前");
map.put(key,value);
TimeUnit.SECONDS.sleep(1);
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()+"准备读数据");
map.get(key);
System.out.println(Thread.currentThread().getName()+"结束读数据");
} catch (Exception e) {
e.printStackTrace();
} finally {
//解开读锁
lock.readLock().unlock();
}
}
}
注意: 读锁和写锁是互斥的,就是说读的时候不能写,写的时候不能读
独占锁(写锁):指该锁一次只能被一个线程锁持有。对于ReentranrLock和 Synchronized 而言都是独
占锁。
共享锁(读锁):该锁可被多个线程所持有。
对于ReentrantReadWriteLock其读锁时共享锁,写锁是独占锁,读锁的共享锁可保证并发读是非常高效
的。
10、阻塞队列
阻塞队列的用处:
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自
动被唤起。
为什么需要 BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue 都
给你一手包办了。
在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要
兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
ArrayBlockingQueue
四组API
检查方法返回的是处于队列首部的元素
抛出异常
public static void main(String[] args) {
ArrayBlockingQueue blockQueue = new ArrayBlockingQueue<>(3);
blockQueue.add("a");
blockQueue.add("b");
blockQueue.add("c");
//add java.lang.IllegalStateException: Queue full 异常
//blockQueue.add(4);
System.out.println(blockQueue.element()); //a
System.out.println(blockQueue.remove());
System.out.println(blockQueue.remove());
System.out.println(blockQueue.remove());
//remove java.util.NoSuchElementException异常
//System.out.println(blockQueue.remove());
}
返回true/false
不会不异常
public static void main(String[] args) {
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a")); // true
System.out.println(blockingQueue.offer("b")); // true
System.out.println(blockingQueue.offer("c")); // true
System.out.println(blockingQueue.offer("d")); // false
System.out.println(blockingQueue.peek()); // 检测队列队首元素!
// public E poll()
System.out.println(blockingQueue.poll()); // a
System.out.println(blockingQueue.poll()); // b
System.out.println(blockingQueue.poll()); // c
System.out.println(blockingQueue.poll()); // null
}
一直阻塞
public static void main(String[] args) throws InterruptedException {
// 队列大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
// 一直阻塞
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
blockingQueue.put("d"); //卡住
System.out.println(blockingQueue.take()); // a
System.out.println(blockingQueue.take()); // b
System.out.println(blockingQueue.take()); // c
System.out.println(blockingQueue.take()); //
}
超时阻塞
public static void main(String[] args) throws InterruptedException {
// 队列大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.offer("a");
blockingQueue.offer("b");
blockingQueue.offer("c");
blockingQueue.offer("d",3L, TimeUnit.SECONDS); // 等待3秒超时退出
System.out.println("====3秒后===");
System.out.println(blockingQueue.poll()); // a
System.out.println(blockingQueue.poll()); // b
System.out.println(blockingQueue.poll()); // c
System.out.println(blockingQueue.poll(3L,TimeUnit.SECONDS)); // 阻塞不停止等待
}
SynchronousQueue 同步队列
SynchronousQueue 容量为1。
与其他的 BlockingQueue 不同,SynchronousQueue是一个不存储元素的 BlockingQueue 。
每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
public class TestSynchro {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new SynchronousQueue<>();
new Thread(()->{
try {
queue.put(1);
System.out.println(Thread.currentThread().getName()+"PUT 1");
queue.put(2);
System.out.println(Thread.currentThread().getName()+"PUT 2");
queue.put(3);
System.out.println(Thread.currentThread().getName()+"PUT 3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"GET"+queue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"GET"+queue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"GET"+queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"B").start();
}
}
11、线程池(重点)
3大方法 7大参数 4中拒绝策略
11.1 池化技术
程序的运行,其本质上,是对系统资源(CPU、内存、磁盘、网络等等)的使用。如何高效的使用这些资源
是我们编程优化演进的一个方向。今天说的线程池就是一种对CPU利用的优化手段。
通过学习线程池原理,明白所有池化技术的基本设计思路。遇到其他相似问题可以解决。
池化技术
前面提到一个名词——池化技术,那么到底什么是池化技术呢 ?
池化技术简单点来说,就是提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化
技术可以大大的提高资源的利用率,提升性能等。
在编程领域,比较典型的池化技术有:
线程池、连接池、内存池、对象池等。
主要来介绍一下其中比较简单的线程池的实现原理,希望读者们可以举一反三,通过对线程池的理解,
学习并掌握所有编程中池化技术的底层原理。
我们通过创建一个线程对象,并且实现Runnable接口就可以实现一个简单的线程。可以利用上多核
CPU。当一个任务结束,当前线程就接收。
但很多时候,我们不止会执行一个任务。如果每次都是如此的创建线程->执行任务->销毁线程,会造成 很大的性能开销。(普通线程结束后就销毁了没法再次start)
那能否一个线程创建后,执行完一个任务后,又去执行另一个任务,而不是销毁。这就是线程池。
这也就是池化技术的思想,通过预先创建好多个线程,放在池中,这样可以在需要使用线程的时候直接
获取,避免多次重复创建、销毁带来的开销。
11.2 为什么使用线程池
10 年前单核CPU电脑,假的多线程,像马戏团小丑玩多个球 ,CPU 需要来回切换。
现在是多核电脑,多个线程各自跑在独立的CPU上,不用切换效率高。
线程池的优势:
线程池做的工作主要是:控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这
些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中
取出任务来执行。
它的主要特点为:线程复用,控制最大并发数,管理线程。
第一:降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系
统的稳定性,使用线程池可以进行统一分配,调优和监控。
11.3 线程池的三大方法
Java中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor ,Executors,
ExecutorService,ThreadPoolExecutor 这几个类。
三大方法说明:
Executors.newFixedThreadPool(int)
- 执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程。
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "结束");
});
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//放再finally中保证一定能偶关闭线程池
threadPool.shutdown();
}
}
//最多同时5个线程在运行
pool-1-thread-1执行
pool-1-thread-5执行
pool-1-thread-4执行
pool-1-thread-3执行
pool-1-thread-2执行
pool-1-thread-4结束
pool-1-thread-5结束
pool-1-thread-1结束
pool-1-thread-3结束
pool-1-thread-1执行
pool-1-thread-4执行
pool-1-thread-2结束
pool-1-thread-3执行
pool-1-thread-5执行
pool-1-thread-2执行
pool-1-thread-5结束
pool-1-thread-4结束
pool-1-thread-3结束
pool-1-thread-2结束
pool-1-thread-1结束
Executors.newSingleThreadExecutor()
- 最多只能有一个线程运行
public class Demo01 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName());
});
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//放再finally中保证一定能偶关闭线程池
threadPool.shutdown();
}
}
}
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
Executors.newCachedThreadPool();
- 执行很多短期异步任务,线程池根据需要创建新线程,但在先构建的线程可用时将重用他们。
可扩容,遇强则强
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 100; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行");
});
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//放再finally中保证一定能偶关闭线程池
threadPool.shutdown();
}
}
11.4 ThreadPoolExecutor 七大参数
操作:查看三大方法的底层源码,发现本质都是调用了 new ThreadPoolExecutor
( 7 个参数 )
//本质都是调用了 `new ThreadPoolExecutor`
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(5, 5,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, //21亿
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
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;
}
参数理解:
corePollSize :核心线程数。在创建了线程池后,线程中没有任何线程,等到有任务到来时才创建线程去执行任务。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
maximumPoolSize :最大线程数。表明线程中最多能够创建的线程数量,此值必须大于等于1。
keepAliveTime :空闲的线程保留的时间。
TimeUnit :空闲线程的保留时间单位。
BlockingQueue< Runnable> :阻塞队列,存储等待执行的任务。参数有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue可选。
ThreadFactory :线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler :队列已满,而且任务量大于最大线程的异常处理策略。有以下取值
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
线程池用哪个?生产中如何设置合理参数
在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多? 坑
答案是一个都不用,我们工作中只能使用自定义的;
Executors 中
ThreadPoolExecutor 底层工作原理
业务例子
- 银行分前台和候客区,前台平时只开两个窗口1和2,如果1和2窗口已经有人了,后进来的客人需要到候客区
进行排队.
2.如果来的客人特别多,导致候客区也满了,这时候银行就要打开3 4 5窗口来处理客人
3.这时候又有客人进来了,如果12345窗口都满了,候客区也满了,新的客人要么走要么等这就是拒绝策略
- 平时开的1和2窗口就是
核心线程池大小
- 候客区的人满了开的3 4 5就是
最大核心线程池大小
- 如果客人处理完业务都走了,一段时间没有人来处理业务,那么就关闭这个线程 这就是
超时等待
- 候客区就是
阻塞队列
手动创建线程池
public class testDemo01 {
public static void main(String[] args) {
// ExecutorService threadPool = Executors.newSingleThreadExecutor();//容量为单个线程的线程池
// ExecutorService threadPool = Executors.newFixedThreadPool(5);//容量为num的线程池
// ExecutorService threadPool = Executors.newCachedThreadPool(); //可伸缩的线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, //默认核心线程数量
5, //最大线程数量
2, //线程等待时间
TimeUnit.SECONDS, //等待时间单位
new ArrayBlockingQueue<>(4), //阻塞队列
Executors.defaultThreadFactory(), //默认即可
new ThreadPoolExecutor.AbortPolicy()); //拒接策略:四种
try {
for (int i = 0; i <= 2; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+":ok");
});
}
} finally {
//最后在finally里面保证线程池关闭
threadPool.shutdown();
}
}
}
11.5 四种拒绝策略
可以看到这个handler有四个实现类
AbortPolicy (默认): 银行满了,还有人进来,不处理这个人的,并抛出异常
DiscardPolicy: 队列满了丢任务,不抛出异常
CallerRunsPolicy: 哪来的回哪去,这里因为是main调用的,所有会执行main线程
DiscardOldestPolicy: 队列满了,将最早进入队列的任务删,之后再尝试加入队列
11.6小结和拓展
池的最大的大小如何去设置!
了解:IO密集型,CPU密集型:(调优)
CPU密集型 : 几核几个线程
IO密集型 : 线程数设置为大于你程序中十分耗IO的线程
一个计算为主的程序(专业一点称为CPU密集型程序)。多线程跑的时候,可以充分利用起所有的cpu
核心,比如说4个核心的cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率。
但是如果线程远远超出cpu核心数量 反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间
的。
因此对于cpu密集型的任务来说,线程数等于cpu数是最好的了。
如果是一个磁盘或网络为主的程序(IO密集型)。一个线程处在IO等待的时候,另一个线程还可以在
CPU里面跑,有时候CPU闲着没事干,所有的线程都在等着IO,这时候他们就是同时的了,而单线程的
话此时还是在一个一个等待的。我们都知道IO的速度比起CPU来是慢到令人发指的。所以开多线程,比
方说多线程网络传输,多线程往不同的目录写文件,等等。
此时 线程数等于IO任务数是最佳的。
public static void main(String[] args) {
// 自定义线程池!工作 ThreadPoolExecutor
// 最大线程到底该如何定义
// 1、CPU 密集型,几核,就是几,可以保持CPu的效率最高!
// 2、IO 密集型 > 判断你程序中十分耗IO的线程,一般是IO线程的两倍
// 程序 15个大型任务 io十分占用资源!
// 获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());
ExecutorService threadPool = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors(),//获取电脑几个核心
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试去和最早的竞争,也不会抛出异常!
try {
// 最大承载:Deque + max
// 超过 RejectedExecutionException
for (int i = 1; i <= 9; i++) {
// 使用了线程池之后,使用线程池来创建线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
12、四大函数式接口(重点必需掌握)
新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算
函数式接口: 只有一个方法的接口
函数型接口 Function
传入一个参数,返回一个参数,泛型中规定参数的类型
public class TestDemo01 {
public static void main(String[] args) {
//函数式接口,可以改为lambda表达式
// Function<String ,Integer> function = new Function<String, Integer>() {
// @Override
// public Integer apply(String s) {
// return 200;
// }
// };
//改写
Function<String,Integer> function = (str)->{
return 200;
};
System.out.println(function.apply("nihao"));
}
}
断定型接口 Predicate
有一个传入参数,返回只有布尔值
// 断定型接口 Predicate
// Predicate<Integer> function = new Predicate<Integer>() {
// @Override
// public boolean test(Integer integer) {
// if(integer>10){
// return true;
// }else {
// return false;
// }
// }
// };
//改造
Predicate<Integer> function = integer -> {
if(integer>10){
return true;
}else {
return false;
}
};
System.out.println(function.test(1));
消费型接口 Consumer
有一个输入参数,没有返回值
//消费型接口,出入参数,不返回任何数据
//Consumer
Consumer<String> function1 =new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
Consumer<String> function2 = (str)->{
System.out.println(str);
};
function1.accept("nia");
function1.accept("666");
供给者接口,Supplier
不接收参数,只有返回值
Supplier<String> function = new Supplier<String>() {
@Override
public String get() {
return "调用了方法";
}
};
Supplier<Integer> supplier = ()->{
return 233;
};
System.out.println(function.get());
System.out.println(supplier.get());
13、Stream流式计算
链式编程、流式计算、lambda表达式,现在的 Java程序员必会!
是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
“集合讲的是数据,流讲的是计算!”
特点:
Stream 自己不会存储元素。
Stream 不会改变源对象,相反,他们会返回一个持有结果的新Stream。
Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
public class Demo02 {
public static void main(String[] args) {
/*
* 题目:请按照给出数据,找出同时满足以下条件的用户
* 也即以下条件:
* 1、全部满足偶数ID
* 2、年龄大于24
* 3、用户名转为大写
* 4、用户名字母倒排序
* 5、只输出一个用户名字 limit
**/
User2 u1 = new User2(11, "a", 23);
User2 u2 = new User2(12, "b", 24);
User2 u3 = new User2(13, "c", 22);
User2 u4 = new User2(14, "d", 28);
User2 u5 = new User2(16, "e", 26);
List<User2> list = Arrays.asList(u1, u2, u3, u4, u5);
/*
* 1. 首先我们需要将 list 转化为stream流
* 2. 然后将用户过滤出来,这里用到一个函数式接口Predicate<? super T>,我们可
以使用lambda表达式简化
* 3. 这里面传递的参数,就是Stream流的泛型类型,也就是User,所以,这里可以直接
返回用户id为偶数的用户信息;
* 4. 通过forEach进行遍历,直接简化输出 System.out::println ,等价于
System.out.println(u);
*/
//链式编程
list.stream()
.filter(u->{return u.getId() % 2 == 0;}) //Predicate
.filter(u->{return u.getAge() > 24;})
.map(u->{return u.getName().toUpperCase();}) //Function接口
.sorted((uu1,uu2)->{return uu2.compareTo(uu1);})//Comparator
.limit(1)
.forEach(System.out::println); //Consumer
}
}
class User2{
private int id;
private String name;
private int age;
public User2() {
}
public User2(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User2{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
14、分支合并
什么是ForkJoin
从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任
务,最终汇总每个小任务的结果得到这个大任务的结果。
这种思想和MapReduce很像(input --> split --> map --> reduce --> output)
主要有两步:
第一、任务切分
第二、结果合并
它的模型大致是这样的:线程池中的每个线程都有自己的工作队列(PS:这一点和ThreadPoolExecutor
不同,ThreadPoolExecutor是所有线程公用一个工作队列,所有线程都从这个工作队列中取任务),当
自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。
工作窃取
另外,forkjoin有一个工作窃取的概念。简单理解,就是一个工作线程下会维护一个包含多个子任务的双
端队列。而对于每个工作线程来说,会从头部到尾部依次执行任务。这时,总会有一些线程执行的速度
较快,很快就把所有任务消耗完了。那这个时候怎么办呢,总不能空等着吧,多浪费资源啊。
工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。工作窃取的运行流程图如
下
那么为什么需要使用工作窃取算法呢?
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程
间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里
的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的
任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干
活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减
少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列
的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还
是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个
双端队列。
于是,先做完任务的工作线程会从其他未完成任务的线程尾部依次获取任务去执行。这样就可以充分利
用CPU的资源。这个非常好理解,就比如有个妹子程序员做任务比较慢,那么其他猿就可以帮她分担一
些任务,这简直是双赢的局面啊,妹子开心了,你也开心了。
核心类
ForkJoinPool
WorkQueue是一个ForkJoinPool中的内部类,它是线程池中线程的工作队列的一个封装,支持任务窃
取。
什么叫线程的任务窃取呢?就是说你和你的一个伙伴一起吃水果,你的那份吃完了,他那份没吃完,那
你就偷偷的拿了他的一些水果吃了。存在执行2个任务的子线程,这里要讲成存在A,B两个个
WorkQueue在执行任务,A的任务执行完了,B的任务没执行完,那么A的WorkQueue就从B的
WorkQueue的ForkJoinTask数组中拿走了一部分尾部的任务来执行,可以合理的提高运行和计算效率。
每个线程都有一个WorkQueue,而WorkQueue中有执行任务的线程(ForkJoinWorkerThread
owner),还有这个线程需要处理的任务(ForkJoinTask<?>[] array)。那么这个新提交的任务就是加
到array中。
ForkJoinTask
ForkJoinTask代表运行在ForkJoinPool中的任务。
主要方法:
fork() 在当前线程运行的线程池中安排一个异步执行。简单的理解就是再创建一个子任务。
join() 当任务完成的时候返回计算结果。
invoke() 开始执行任务,如果必要,等待计算完成。
子类: Recursive :递归
RecursiveAction 一个递归无结果的ForkJoinTask(没有返回值)
RecursiveTask 一个递归有结果的ForkJoinTask(有返回值)
代码验证
package org.example.juc.forkjoin;
import java.util.concurrent.RecursiveTask;
//计算1-10000000000的和
public class ForkJoinTest extends RecursiveTask<Long> {
private Long start;//起始值
private Long end;//结束值
public static final Long critical = 10000L;//临界值
public ForkJoinTest(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
//判断是否是拆分完毕
Long lenth = end - start;
//小于临界值就不拆分
if(lenth<=critical){
//如果拆分完毕就相加
Long sum = 0L;
for (Long i = start;i<=end;i++){
sum += i;
}
return sum;
}else {
//没有拆分完毕就开始拆分
Long middle = (end + start)/2;//计算的两个值的中间值
ForkJoinTest right = new ForkJoinTest(start,middle);
right.fork();//拆分,并压入线程队列
ForkJoinTest left = new ForkJoinTest(middle+1,end);
left.fork();//拆分,并压入线程队列
//合并
return right.join() + left.join();
}
}
}
三种测试
package org.example.juc.forkjoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
test2();
test3();
}
//普通
public static void test1(){
Long sum = 0L;
long start = System.currentTimeMillis();
for (long i = 0; i < 10_0000_0000L; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("sum= "+(end-start));
}
//forkjoin
public static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
//生成任务
ForkJoinTask<Long> task = new ForkJoinTest(1L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(task); //提交任务
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("sum= "+(end-start));
}
//stream 并行流 比上面快几十倍
public static void test3(){
long start = System.currentTimeMillis();
//parallel() 并行
long sum = LongStream.rangeClosed(0L, 10_0000_1000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum= "+(end-start));
}
}
打个比方,假设一个酒店有400个房间,一共有4名清洁工,每个工人每天可以打扫100个房间,这样,4
个工人满负荷工作时,400个房间全部打扫完正好需要1天。
Fork/Join的工作模式就像这样:首先,工人甲被分配了400个房间的任务,他一看任务太多了自己一个
人不行,所以先把400个房间拆成两个200,然后叫来乙,把其中一个200分给乙。
紧接着,甲和乙再发现200也是个大任务,于是甲继续把200分成两个100,并把其中一个100分给丙,
类似的,乙会把其中一个100分给丁,这样,最终4个人每人分到100个房间,并发执行正好是1天。
16、JMM
问题:请你谈谈你对volatile的理解
volatile
是 Java 虚拟机提供的轻量级的同步机制,三大特性:
保证可见性
不保证原子性
禁止指令重排
16.1 什么是JMM
JMM 本身是一种抽象的概念,并不真实存在,它描述的是一组规则或者规范~
JMM 关于同步的规定:
1、线程解锁前,必须立刻把共享变量的值刷新回主内存
2、线程加锁前,必须读取主内存的最新值到自己的工作内存
3、加锁解锁是同一把锁
JMM即为JAVA 内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从java 5开始的JSR-133发布后,已经成熟和完善起来。
JMM规定了内存主要划分为主内存和工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。
16.2 JMM的内存模型
线程A感知不到线程B操作了值的变化!如何能够保证线程间可以同步感知这个问题呢?只需要使用
Volatile关键字即可!volatile 保证线程间变量的可见性,
简单地说就是当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动,
更详细地说是要符合以下两个规则 :
线程对变量进行修改之后,要立刻回写到主内存。
线程对变量读取的时候,要从主内存中读,而不是缓存。
各线程的工作内存间彼此独立,互不可见,在线程启动的时候,虚拟机为每个内存分配一块工作内存,
不仅包含了线程内部定义的局部变量,也包含了线程所需要使用的共享变量(非线程内构造的对象)的
副本,即,为了提高执行效率。
16.3 内存交互操作
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类
型的变量来说,load、store、read和write操作在某些平台上允许例外)
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
不允许一个线程将没有assign的数据从工作内存同步回主内存
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
对一个变量进行unlock操作之前,必须把此变量同步回主内存
JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不
安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分
析。更多的时候,使用java的happen-before规则来进行分析。
happens-before字面翻译过来就是先行发生,A happens-before B 就是A先行发生于B?
不准确!在Java内存模型中,happens-before 应该翻译成:前一个操作的结果可以被后续的操作获取。 讲白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。
我们再来看看为什么需要这几条规则?
因为我们现在电脑都是多CPU,并且都有缓存,导致多线程直接的可见性问题。详情可以看我之前的文章
面试官:你知道并发Bug的源头是什么吗?
所以为了解决多线程的可见性问题,就搞出了happens-before原则,让线程之间遵守这些原则。编译器
还会优化我们的语句,所以等于是给了编译器优化的约束。不能让它优化的不知道东南西北了
//这个程序不会停下来,因为线程A不知道num已经被改成1了
private static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while (num == 0){
}
},"A").start();
//保证线程开启了
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//想要让线程A知道我们主线程修改了num的值从而停下来
num = 1;
}
17. Volatile
保证可见性
volatile
是 Java 虚拟机提供的轻量级的同步机制,三大特性:
保证可见性
不保证原子性
禁止指令重排
//添加volitile后A线程能够看到主线程对num进行了修改
public class JMMDemo {
//不加volatile就会死循环
private volatile static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while (num == 0){
}
},"A").start();
//保证线程开启了
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//想要让线程A知道我们主线程修改了num的值从而停下来
num = 1;
}
}
不保证原子性
原子性理解:
不可分割,完整性,也就是某个线程正在做某个具体的业务的时候,中间不可以被加塞或者被分割,需
要整体完整,要么同时成功,要么同时失败。
package org.example.juc.jmm;
public class VDemo {
//volatile 保证不了原子性 synchronized和lock能保证
private volatile static int num = 0;
public static void add(){
num++;
}
public static void main(String[] args) {
// 理论上结果为20000
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
//add在汇编中并不是一条语句,不具备原子性
add();
}
}).start();
}
while (Thread.activeCount() > 2){ // main gc
Thread.yield();
}
System.out.println(num); //17114
}
}
因为我们的 add 方法没有加锁,但是加了 volatile ,说明 volatile 不能保证原子性;画图解释,数值被
覆盖
不用synchronized和lock如何能保证原子性
使用java.util.concurrent.atomic下的类
package org.example.juc.jmm;
import java.util.concurrent.atomic.AtomicInteger;
public class VDemo {
//volatile 保证不了原子性 synchronized和lock能保证
//使用AtomicInteger保证原子性
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
num.getAndIncrement();//+1
}
public static void main(String[] args) {
// 理论上结果为20000
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2){ // main gc
Thread.yield();
}
System.out.println(num); //17114
}
}
禁止指令重排
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的数据依赖性
。
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
重排理解测试1:
public class TestHappensBefore {
public static void main(String[] args) {
int x = 11; // 语句1
int y = 12; // 语句2
x = x + 5; // 语句3
y = x * x; // 语句4
}
// 指令顺序预测: 1234 2134 1324
// 问题:请问语句4可以重排后变成第一条吗? 答案:不可以
}
重排理解测试2:
// 多线程环境中线程交替执行,由于编译器优化重排的存在
// 两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
public class TestHappensBefore {
int a = 0;
boolean flag = false;
public void m1(){
a = 1; // 语句1
flag = true; // 语句2
}
public void m2(){
if (flag){
a = a + 5; // 语句3
System.out.println("m2=>"+a);
}
}
}
指令重排小结:
volatile 实现了禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU 指令,它的作用有两个:
1、保证特定操作的执行顺序。
2、保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条 Memory Barrier 则会告诉编译器
和CPU,不管什么指令都不能和这条 Memory Barrier 指令重排序,也就是说,通过插入内存屏障禁止
在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此
任何CPU上的线程都能读取到这些数据的最新版本。
保证了可见性
经过,可见性,原子性,指令重排的话,线程安全性获得保证:
工作内存与主内存同步延迟现象导致的可见性问题,可以使用 synchronized 或 volatile 关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
对于指令重排导致的可见性问题 和 有序性问题,可以利用 volatile 关键字解决,因为 volatile 的另外一个作用就是禁止重排序优化。
volatile在懒汉式的单例模式中使用,所以接下来聊聊单例模式
18、深入单例模式
单例模式可以说只要是一个合格的开发都会写,但是如果要深究,小小的单例模式可以牵扯到很多东西,比如 多线程是否安全,是否懒加载,性能等等。还有你知道几种单例模式的写法呢?如何防止反射破坏单例模式?今天,我们来探究单例模式。
关于单例模式的概念,在这里就不在阐述了,相信每个小伙伴都了如指掌。我们直接进入正题:
18.1 饿汉式
//饿汉式单例模式
class Hungry{
//构造器私有化
public Hungry(){}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
饿汉式是最简单的单例模式的写法,保证了线程的安全
,在很长的时间里,我都是饿汉模式来完成单例的,因为够简单,后来才知道饿汉式会有一点小问题,看下面的代码:
public class Hungry {
private byte[] data1 = new byte[1024];
private byte[] data2 = new byte[1024];
private byte[] data3 = new byte[1024];
private byte[] data4 = new byte[1024];
private Hungry() {
}
private final static Hungry hungry = new Hungry();
public static Hungry getInstance() {
return hungry;
}
}
在Hungry类中,我定义了四个byte数组,当代码一运行,这四个数组就被初始化,并且放入内存了,如果长时间没有用到getInstance方法,不需要Hungry类的对象,这不是一种浪费吗?我希望的是 只有用到了 getInstance方法,才会去初始化单例类,才会加载单例类中的数据。所以就有了 第二种单例模式:懒汉式。
18.2 DCL懒汉式
正常的懒汉 存在多线程问腿
public class LazyMan {
public LazyMan() {
System.out.println(Thread.currentThread().getName());
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance(){
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
多加一层检测可以避免问题,也就是DCL懒汉式
!
public class LazyMan {
public LazyMan() {
System.out.println(Thread.currentThread().getName());
}
//volatile防止指令重排
private volatile static LazyMan lazyMan;
//双重检测锁模式 懒汉式单例 DCL懒汉式
public static LazyMan getInstance(){
if (lazyMan == null){
synchronized (LazyMan.class){
if(lazyMan == null){
lazyMan = new LazyMan();
/**
* 1. 分配内存空间
* 2. 执行构造方法,初始化对象
* 3. 把这个对象指向这个空间
*
* 可能发生指令重排 我们需要给lazyMan加上volatile
*/
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
这是因为编译器在编译时会进行指令重排,而 volatile 可以禁止指令重排。
对象的创建大概有这么三个步骤(从指令层面来看啊,从JVM来讲具体还有很多步骤):
分配内存空间
执行构造方法,初始化对象
把这个对象指向这个空间
正常是按 123 的顺序执行,但由于指令重排的存在,可能会存在 A 线程按照132 执行,当执行到 3 时,线程 B 来取对象,就会得到空值。因此必须给单例对象加上volatile关键字。
18.3 静态内部类
还有这种方式是第一种饿汉式的改进版本,同样也是在类中定义static变量的对象,并且直接初始化,不过是移到了静态内部类中,十分巧妙。既保证了线程的安全性,同时又满足了懒加载。
public class Holder {
private Holder() {
}
public static Holder getInstance() {
return InnerClass.holder;
}
private static class InnerClass {
private static final Holder holder = new Holder();
}
}
18.4 万恶的反射
万恶的反射登场了,反射是一个比较霸道的东西,无视private修饰的构造方法,可以直接在外面
newInstance,破坏我们辛辛苦苦写的单例模式。
并没有一个很好的方案去避免反射破坏单例模式,所以轮到我们的枚举登场了。
18.5 枚举(推荐)
枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类
(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性
以及便捷性。
package org.example.juc.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum EnumSingle {
INSTANCE; //枚举类对象
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
EnumSingle enumSingle1 = EnumSingle.INSTANCE;
//报错,IDEA显示有空参构造,但是实际上没有空参构造,需要用jad看,发现有个string,int的构造
// Constructor<EnumSingle> declaredConstructor =
// EnumSingle.class.getDeclaredConstructor(null);
//报错,不能使用反射破环,这种才是我们想要的
Constructor<EnumSingle> declaredConstructor2 =
EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor2.setAccessible(true);
EnumSingle enumSingle2 = declaredConstructor2.newInstance();
System.out.println(enumSingle1);
System.out.println(enumSingle2);
}
}
枚举是目前最推荐的单例模式的写法
,因为足够简单,不需要开发自己保证线程的安全,同时又可以有
效的防止反射破坏我们的单例模式.
19、深入理解CAS
CAS : 比较并交换
前言:互联网缩招之下,初级程序员大量过剩,高级程序员重金难求,除非你不吃这碗饭,否则就要逼
自己提升!
用代码理解下什么是CAS CompareAndSet
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 期望、更新
// public final boolean compareAndSet(int expect, int update)
// 如果我期望的值达到了,那么就更新,否则,就不更新, CAS 是CPU的并发原语!``
System.out.println(atomicInteger.compareAndSet(2020, 2021)); //true
System.out.println(atomicInteger.get());//2021
atomicInteger.getAndIncrement(); //2022
System.out.println(atomicInteger.compareAndSet(2020, 2021)); //false
System.out.println(atomicInteger.get());
}
看下源码
getAndIncrement使用的是unsafe类
- this–当前对象
- valueOffset-- 表示内存地址的偏移值
- 1 —增加的值
进入unsafe看下unsafa由Unsafe类创建
点进GetAndAddInt看下
var5 获取 当前的值var2
然后循环比较当前的值和var5是否相等,如果相等就加上var4
这就是一个自旋锁,不断的循环进行判断
问题:这个UnSafe类到底是什么? 可以看到AtomicInteger源码中也是它!
1.UnSafe
UnSafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,
UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类存在于 sun.misc包中,其
内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意:Unsafe类中的所有方法都是Native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层
资源执行相应任务
2.变量valueOffset
表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
3.变量 value用volatile修饰,保证了多线程之间的内存可见性
最后解释CAS 是什么
CAS 的全称为 Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是 sun.misc.Unsafe 类中的各个方法。调用UnSafe类中的CAS方法,
JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强
调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功
能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU
的原子指令,不会造成所谓的数据不一致问题。
汇编层面理解
总结
CAS(CompareAndSwap)
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作
内存中的值一致为止。
CAS 应用
CAS 有3个操作数,内存值V,旧的预期值A,要修改的更新值B。且仅当预期值A 和 内存值 V 相同时,
将内存值 V 修改为B,否则什么都不做。
CAS 的缺点
循环时间长开销很大。
可以看到源码中存在 一个 do…while 操作,如果CAS失败就会一直进行尝试。
只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作。但是:
对多个共享变量操作时,循环CAS就无法保证操作的原子性,这时候就可以用锁来保证原子性。
引出来 ABA 问题(狸猫换太子)???
ABA问题
- 两个线程同时操作A变量
- 左边线程期待A=1,但是右边线程把A先变成了3然后又变成了1
- 左边线程对A被修改过了完全不知情
public class CASDemo {
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 期望、更新
// public final boolean compareAndSet(int expect, int update)
// 如果我期望的值达到了,那么就更新,否则,就不更新, CAS 是CPU的并发原语!
// ============== 捣乱的线程 ==================
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
// ============== 期望的线程 ==================
System.out.println(atomicInteger.compareAndSet(2020, 6666));
System.out.println(atomicInteger.get());
}
}
20、原子引用
原子类 AtomicInteger 的ABA问题谈谈?原子更新引用知道吗?
解决ABA 问题,引入原子引用 AtomicReference! 也就是加入版本号, 对应的思想:乐观锁!
演示ABA问题: AtomicReference
/**
* ABA 问题的解决 AtomicStampedReference
*/
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"T1").start();
new Thread(()->{
// 暂停一秒钟,保证上面线程先执行
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//已经被修改了,却不知道
System.out.println(atomicReference.compareAndSet(100, 2019)); //修改成功!
System.out.println(atomicReference.get());
},"T2").start();
}
}
解决方案:AtomicStampedReference 和乐观锁的原理相同
package org.example.juc.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class CASDemo01 {
//默认值为1 ,默认版本号为1
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);//注意这里的Integer默认值得在-128~127之间,不然会重新创建对象
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
new Thread(()->{
//获取当前版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("a1=>开始时的版本号"+stamp);
//把1修改为2
System.out.println(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());
}).start();
new Thread(()->{
//获取当前版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("b1=>开始时的版本号"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//把1修改为2 修改失败,因为这里期待的stamp为1 但是实际上已经被另一个线程修改了
System.out.println(atomicStampedReference.compareAndSet(1, 2,
stamp, stamp + 1));
System.out.println("b1=>版本号为"+atomicStampedReference.getStamp());
}).start();
}
}
- Java锁
21.1 公平锁 非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
非公平锁(默认):是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比现申请的线程
优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。
//设置公平锁 非公平锁
// 无参
public ReentrantLock() {
sync = new NonfairSync();
}
// 有参
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
区别
并发包中的 ReentrantLock 的创建可以指定构造函数 的 boolean类型来得到公平锁或者非公平锁,默认
是非公平锁!
公平锁:就是很公平,在并发环境中,每个线程在获取到锁时会先查看此锁维护的等待队列,如果为
空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规
则从队列中取到自己。
非公平锁:非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就会采用类似公平锁那种方
式。
Java ReentrantLock 而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在
于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。
21.2 可重入锁
可重入锁(也叫递归锁)
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法 获取锁的时候,在进入内层方法会自动获取锁。
也就是说,线程可以进入任何一个它已经拥有的锁,所同步着的代码块。 好比家里进入大门之后,就可
以进入里面的房间了;
ReentrantLock、Synchronized 就是一个典型的可重入锁;
可重入锁最大的作用就是避免死锁
Synchronized版
package org.example.juc.relock;
import java.util.concurrent.TimeUnit;
/**
* 可重入锁(也叫递归锁)
* 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码
* 在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
*/
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(() -> {
phone.sms();
},"A").start();
new Thread(() -> {
phone.sms();
},"B").start();
}
/**
* A sms
* A call
* B sms
* B call
*/
}
class Phone{
public synchronized void sms(){
System.out.println(Thread.currentThread().getName() + " sms");
call();//这里也有锁
}
private synchronized void call() {
System.out.println(Thread.currentThread().getName() + " call");
}
}
Lock版
/**
* 可重入锁(也叫递归锁)
* 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码
* 在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
*/
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(() -> {
phone.sms();
},"A").start();
new Thread(() -> {
phone.sms();
},"B").start();
}
}
class Phone{
private Lock lock = new ReentrantLock();
public void sms(){
//细节:
// 1.sum方法和call方法中都有lock锁,sms在这里其实就拿到了call中的锁了
// 2.lock 和 unlock必须配对,否则会死锁
// 3.Lock的对象是可以复用的,及可以多次lock()
lock.lock();
lock.lock(); //lock几次下面必须unlock几次
try {
System.out.println(Thread.currentThread().getName() + " sms");
call();//这里也有锁
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
lock.unlock();
}
}
private void call() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " call");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
21.3 自旋锁
自旋锁(spinlock)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下
文切换的消耗,缺点是循环会消耗CPU。
前面在CAS中遇到的其实就是自旋锁
package org.example.juc.spinlock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
public class SpinLockDemo {
//int 0
//Thread null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "线程进入到mylock");
//自旋锁
while(!atomicReference.compareAndSet(null,thread)){
}
}
//解锁
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "线程离开到mylock");
atomicReference.compareAndSet(thread,null);
}
}
class SpinLockTest{
public static void main(String[] args) throws InterruptedException {
// ReentrantLock reentrantLock = new ReentrantLock();
// reentrantLock.lock();
// reentrantLock.unlock();
//底层使用自旋锁CAS
SpinLockDemo lock = new SpinLockDemo();
new Thread(()->{
lock.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.myUnLock();
}
},"A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
lock.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.myUnLock();
}
},"B").start();
/**
* A线程进入到mylock
* B线程进入到mylock //进入的时候还没拿锁
* A解锁后B才能加锁,因为A解锁后线程才会变为null,B才有机会拿到锁
* A线程离开到mylock
* B线程离开到mylock
*
*/
}
}
21.4 死锁
死锁是什么
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干
涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性
就很低,否者就会因为争夺有限的资源而陷入死锁。
产生死锁主要原因:
1、系统资源不足
2、进程运行推进的顺序不合适
3、资源分配不当
package org.example.juc.deadlock;
import java.util.concurrent.TimeUnit;
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new MyThread(lockA,lockB),"T1").start();
new Thread(new MyThread(lockB,lockA),"T2").start();
}
}
class MyThread implements Runnable{
private String lockA;
private String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName() + "拥有lock:" + lockA + "想要lock:" + lockB);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName() + "拥有lock:" + lockB + "想要lock:"+lockA);
}
}
}
}
解决
拓展java自带工具操作:
- 查看JDK目录的bin目录
- 使用
jps -l
命令定位进程号
- 使用
jstack 进程号
找到死锁查看
可以看到这两个线程互相拿着一个锁,然后要对方的锁
问10个人,9个说看日志,还有一个分析堆栈信息,这一步,他就已经赢了!
new Thread(() -> {
phone.sms();
},“B”).start();
}
}
class Phone{
private Lock lock = new ReentrantLock();
public void sms(){
//细节:
// 1.sum方法和call方法中都有lock锁,sms在这里其实就拿到了call中的锁了
// 2.lock 和 unlock必须配对,否则会死锁
// 3.Lock的对象是可以复用的,及可以多次lock()
lock.lock();
lock.lock(); //lock几次下面必须unlock几次
try {
System.out.println(Thread.currentThread().getName() + " sms");
call();//这里也有锁
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
lock.unlock();
}
}
private void call() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " call");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
### 21.3 自旋锁
自旋锁(spinlock)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下
文切换的消耗,缺点是循环会消耗CPU。
前面在CAS中遇到的其实就是自旋锁
[外链图片转存中...(img-67EjLXqa-1716643973628)]
```java
package org.example.juc.spinlock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
public class SpinLockDemo {
//int 0
//Thread null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "线程进入到mylock");
//自旋锁
while(!atomicReference.compareAndSet(null,thread)){
}
}
//解锁
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "线程离开到mylock");
atomicReference.compareAndSet(thread,null);
}
}
class SpinLockTest{
public static void main(String[] args) throws InterruptedException {
// ReentrantLock reentrantLock = new ReentrantLock();
// reentrantLock.lock();
// reentrantLock.unlock();
//底层使用自旋锁CAS
SpinLockDemo lock = new SpinLockDemo();
new Thread(()->{
lock.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.myUnLock();
}
},"A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
lock.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.myUnLock();
}
},"B").start();
/**
* A线程进入到mylock
* B线程进入到mylock //进入的时候还没拿锁
* A解锁后B才能加锁,因为A解锁后线程才会变为null,B才有机会拿到锁
* A线程离开到mylock
* B线程离开到mylock
*
*/
}
}
21.4 死锁
死锁是什么
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干
涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性
就很低,否者就会因为争夺有限的资源而陷入死锁。
[外链图片转存中…(img-MHSw6dVV-1716643973629)]
产生死锁主要原因:
1、系统资源不足
2、进程运行推进的顺序不合适
3、资源分配不当
package org.example.juc.deadlock;
import java.util.concurrent.TimeUnit;
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new MyThread(lockA,lockB),"T1").start();
new Thread(new MyThread(lockB,lockA),"T2").start();
}
}
class MyThread implements Runnable{
private String lockA;
private String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName() + "拥有lock:" + lockA + "想要lock:" + lockB);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName() + "拥有lock:" + lockB + "想要lock:"+lockA);
}
}
}
}
解决
拓展java自带工具操作:
- 查看JDK目录的bin目录
- 使用
jps -l
命令定位进程号
[外链图片转存中…(img-XrxN6zGO-1716643973629)]
- 使用
jstack 进程号
找到死锁查看
[外链图片转存中…(img-wrZ17r1N-1716643973630)]
可以看到这两个线程互相拿着一个锁,然后要对方的锁
问10个人,9个说看日志,还有一个分析堆栈信息,这一步,他就已经赢了!