🌭回顾一下多线程-贰
回顾一下多线程-贰
线程同步-synchronized
多个线程操作同一资源时会有问题出现,用
synchronized
同步.线程同步的形成条件:
队列+锁
实现形式有 同步方法 和 同步代码块
同步方法, 锁的是方法所属对象 this
如下买票例子中 buy 方法锁的是
Ticket ticket
这个对象public synchronized void buy() {}
同步代码块, 锁 obj
synchronized (obj) {
// 操作
}
买票
public class Ticket implements Runnable { |
结果
不加锁: 有错误
小明-->得到倒数第10票
老师-->得到倒数第9票
黄牛-->得到倒数第8票
老师-->得到倒数第7票
小明-->得到倒数第6票
黄牛-->得到倒数第5票
老师-->得到倒数第4票
小明-->得到倒数第3票
黄牛-->得到倒数第3票
老师-->得到倒数第2票
小明-->得到倒数第1票
黄牛-->得到倒数第1票加锁: 无误
小明-->得到倒数第10票
黄牛-->得到倒数第9票
老师-->得到倒数第8票
小明-->得到倒数第7票
黄牛-->得到倒数第6票
老师-->得到倒数第5票
黄牛-->得到倒数第4票
小明-->得到倒数第3票
老师-->得到倒数第2票
黄牛-->得到倒数第1票
银行取款
import java.math.BigDecimal; |
预期结果
A取走50
我的账户余额为:50
A手里的钱:50
我的账户钱不够100,B无法取走
关于试错技巧
熟练使用.sleep()试错
试错前
我的账户钱不够100,B无法取走
我的账户余额为:50
A手里的钱:50
试错后
B 取走 100
A 取走 50
我的账户余额为:50
我的账户余额为:50
A 手里的钱:50
B 手里的钱:100
集合与线程安全
多个线程同时操作集合对象时可能会存在覆写(线程不安全)
非线程同步
public class TestList { |
线程同步
public class TestList { |
线程安全集合
public class JUC { |
死锁
两线程在各自拥有一个对象的锁时都等待对方线程释放对象的锁 ; 也有可能很多线程产生环形/网状死锁.
如下例子就会产生死锁
//死锁:多个线程互相抱着对方需要的资源
public class DeadLock {
public static void main(String[] args) {
Makeup m1 = new Makeup(0, "小黑");
Makeup m2 = new Makeup(1, "小白");
m1.start();
m2.start();
}
}
//口红
class Lipstick {
}
//镜子
class Mirror {
}
//化妆
class Makeup extends Thread {
//需要的资源只有一份,用static来保证只有一份
static final Lipstick lipstick = new Lipstick();
static final Mirror mirror = new Mirror();
int choice; //选择
String name; //使用化妆品的人
public Makeup(int choice, String name) {
this.choice = choice;
this.name = name;
}
public void run() {
//化妆
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//化妆,互相持有对方的锁,就是需要拿到对方的资源
private void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) { //获得口红的锁
System.out.println(this.name + "获得口红的锁");
Thread.sleep(1000);
synchronized (mirror) { //一秒钟后想获得镜子的锁
System.out.println(this.name + "获得镜子的锁");
}
}
} else {
synchronized (mirror) { //获得镜子的锁
System.out.println(this.name + "获得镜子的锁");
Thread.sleep(2000);
synchronized (lipstick) { //两秒钟后想获得口红的锁
System.out.println(this.name + "获得口红的锁");
}
}
}
}
}
产生条件
四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
解决方案
使用完同步对象后立即释放
- 比如上面的例子中使用完
口红
或者镜子
后未释放,再去获取另一个对象的锁,就会产生死锁了 - 修改: 把 makeup 改为如下
private void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) { // 获得口红的锁
System.out.println(this.name + "获得口红的锁");
Thread.sleep(1000);
}
synchronized (mirror) { // 一秒钟后想获得镜子的锁
System.out.println(this.name + "获得镜子的锁");
}
} else {
synchronized (mirror) { // 获得镜子的锁
System.out.println(this.name + "获得镜子的锁");
Thread.sleep(2000);
}
synchronized (lipstick) { // 两秒钟后想获得口红的锁
System.out.println(this.name + "获得口红的锁");
}
}
}- 比如上面的例子中使用完
可重入锁-ReentrantLock
ReentrantLock (也叫 RT-Lock) 类实现了 java.util.concurrent.locks.Lock 接口
与 synchronized 区别:
ReentrantLock 是显式加解锁,它只能锁代码块
性能比 synchronized 好
使用优先度: ReentrantLock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
注意点: 加解锁最好要在 try-finally 里
try {
while (true) {
lock.lock();
try {
if ( ) {
// ...
} else {
// ...
}
} catch (InterruptedException ignored) {
} finally {
lock.unlock();
}
}例子
public class TestLock implements Runnable {
int ticketNums = 10;
private final ReentrantLock lock = new ReentrantLock(); // 定义Lock锁
public void run() {
while (true) {
try {
lock.lock(); // 加锁
if (ticketNums > 0) {
System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "票");
} else {
break;
}
} finally {
lock.unlock(); // 解锁
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
TestLock testLock = new TestLock();
new Thread(testLock, "a").start();
new Thread(testLock, "b").start();
new Thread(testLock, "c").start();
}
}结果
a-->拿到了第10票
c-->拿到了第9票
b-->拿到了第8票
b-->拿到了第7票
c-->拿到了第6票
a-->拿到了第5票
b-->拿到了第4票
a-->拿到了第3票
c-->拿到了第2票
c-->拿到了第1票
多线程与循环控制
public class TestLock implements Runnable { |
上面代码 while 循环会存在判断失误
c-->拿到了第10票
c-->拿到了第9票
c-->拿到了第8票
c-->拿到了第7票
c-->拿到了第6票
c-->拿到了第5票
c-->拿到了第4票
c-->拿到了第3票
c-->拿到了第2票
c-->拿到了第1票
b-->拿到了第0票
a-->拿到了第-1票ticketNums 在判断之后被多次修改
上面 10~1 次都是 c 线程执行的,它执行后轮到 b 和 a
但是 b 与 a 线程实际上是在
ticketNums=10
时进入的循环,所以会导致-1
出现所以建议遇到多线程循环控制时,直接
while(true)
,然后在内部用if
(反过来想: 是因为 lock 不能在循环之外加)
延迟对多线程的影响
下面三个例子数据都没错,关键看并发数量和执行时间
一
瞬间执行完,资源被单一线程全部抢占 (并非不合理,只不过是处理器没分配到 B,C)
public class TestLock implements Runnable {
int ticketNums = 10;
private final ReentrantLock lock = new ReentrantLock(); // 定义Lock锁
public void run() {
while (true) {
try {
lock.lock(); // 加锁
if (ticketNums > 0) {
System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "票");
} else {
break;
}
} finally {
lock.unlock(); // 解锁
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}
public static void main(String[] args) {
TestLock testLock = new TestLock();
new Thread(testLock, "a").start();
new Thread(testLock, "b").start();
new Thread(testLock, "c").start();
}
}c-->拿到了第9票
c-->拿到了第8票
c-->拿到了第7票
c-->拿到了第6票
c-->拿到了第5票
c-->拿到了第4票
c-->拿到了第3票
c-->拿到了第2票
c-->拿到了第1票
二
给他加个延迟试试: 三线程并行,资源分配合理
public void run() {
while (true) {
try {
lock.lock(); // 加锁
if (ticketNums > 0) {
System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "票");
} else {
break;
}
} finally {
lock.unlock(); // 解锁
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}b-->拿到了第10票
a-->拿到了第9票
c-->拿到了第8票
b-->拿到了第7票
a-->拿到了第6票
c-->拿到了第5票
c-->拿到了第4票
a-->拿到了第3票
b-->拿到了第2票
b-->拿到了第1票
三
再试试延迟之后解开同步锁: 单线执行,资源分配不平衡
public void run() {
while (true) {
try {
lock.lock(); // 加锁
if (ticketNums > 0) {
System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "票");
} else {
break;
}
} finally {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock(); // 解锁
}
}
}a-->拿到了第10票
a-->拿到了第9票
c-->拿到了第8票
c-->拿到了第7票
c-->拿到了第6票
c-->拿到了第5票
c-->拿到了第4票
c-->拿到了第3票
c-->拿到了第2票
b-->拿到了第1票
线程通信-wait-notify
class Clerk { |
结果
产品为0, 开始生产
库存: 0
库存: 1
库存: 2
库存: 3
库存: 4
产品为5,开始消费
库存: 5
库存: 4
库存: 3
库存: 2
库存: 1
产品为0, 开始生产
库存: 0
线程池
线程池的出现是为了方便大量的线程创建,回收和管理
需要了解
ExecutorService
线程池接口;以及Executors
线程池工具类.corePoolSize: 核心池的大小
maximumPoolSize:最大线程数
keepAliveTime: 线程没有任务时最多保持多长时间后会终止
void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执 Runnable
Future submit(Callable task) :执行任务,有返回值,一般用来执行 Callable
例子
public class TestThreadPool {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1.创建服务,创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName() + " runnable");
};
Callable<String> callable = () -> {
return Thread.currentThread().getName() + " callable";
};
// 2. 执行
for (int i = 0; i < 5; i++) {
service.execute(runnable);
System.out.println(service.submit(callable).get());
}
// 3.关闭连接
service.shutdown();
}
}
/*
pool-1-thread-1 runnable
pool-1-thread-2 callable
pool-1-thread-3 runnable
pool-1-thread-4 callable
pool-1-thread-5 runnable
pool-1-thread-6 callable
pool-1-thread-7 runnable
pool-1-thread-8 callable
pool-1-thread-9 runnable
pool-1-thread-10 callable
*/