目录
死锁
可以看这篇:https://blog.csdn.net/hd12370/article/details/82814348
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:
public class deadlock {
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();
new Thread() {
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread() {
@Override
public void run() {
synchronized (s2) {
s1.append("b");
s2.append("3");
synchronized (s1) {
s1.append("c");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
}
}
结果,两种可能
ab
12
abbc
1234
bc
34
bcab
3412
如果都加上sleep(100) ,就会出现死锁
public class deadlock {
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();
new Thread() {
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//需要得到s2这把锁,下面的线程却在使用,等待
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread() {
@Override
public void run() {
synchronized (s2) {
s1.append("b");
s2.append("3");
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//需要得到s1这把锁,上面的线程却在使用,等待
synchronized (s1) {
s1.append("c");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
}
}
结果:因为互相等待,所以导致死锁,不打印。
锁:可重入的互斥锁 Lock
这篇文章写得不错看这篇:https://www.cnblogs.com/takumicx/p/9338983.html
可重入:https://www.cnblogs.com/gxyandwmm/p/9387833.html
可重入就是,就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值(state)进行累加(+1),当前线程的state为0了,其他线程(如果是非公平锁策略,包括自己)才可以来竞争这把锁。
还是那个窗口售票的例子,只要两步就可以实现多线程问题
public class LockTest {
public static void main(String[] args) {
window window = new window();
Thread thread1 = new Thread(window);
Thread thread2 = new Thread(window);
Thread thread3 = new Thread(window);
thread1.start();
thread2.start();
thread3.start();
}
}
class window implements Runnable {
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (ticket > 0) {
buy();
}
}
public void buy() {
try {
//获取锁
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + ticket--);
}
} finally {
//释放锁
lock.unlock();
}
}
}
多线程练习题
银行有一个账户,有两个储户分别向同一个账户存3000元,每次存1000, 存3次。每次存完打印账户余额。
class Account {
//默认原始账户余额0元
private int balance = 0;
public void deposit(int money) {
if (money > 0) {
balance += money;
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "存款了"+money+"元,当前账户余额:" + balance);
}
}
}
class Customer extends Thread {
//使用继承Thread ,账户应共享一个,所以公用Account 对象
private Account account;
public Customer(Account account) {
this.account = account;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
account.deposit(1000);
}
}
}
public class Bank {
public static void main(String[] args) {
Account account = new Account();
Customer c1 = new Customer(account);
Customer c2 = new Customer(account);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
结果:明显不正确,第一次打印应该是余额1000元 等等,因为在睡眠1秒的过程中当前线程进入阻塞态,其他线程进来了,变为运行态,这样,输出语句没来得及打印,账户就被加了2次1000。
乙存款了1000元,当前账户余额:2000
甲存款了1000元,当前账户余额:2000
甲存款了1000元,当前账户余额:4000
乙存款了1000元,当前账户余额:5000
乙存款了1000元,当前账户余额:6000
甲存款了1000元,当前账户余额:6000
解决办法,加锁,为啥ReentrantLock 不用加static呢,因为两个线程同步监视器都是accout对象。
class Account {
//默认原始账户余额0元
private int balance = 0;
private ReentrantLock lock = new ReentrantLock();
public void deposit(int money) {
//加锁
lock.lock();
if (money > 0) {
balance += money;
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "存款了" + money + "元,当前账户余额:" + balance);
}
//解锁
lock.unlock();
}
}
线程通信:wait(),notifyAll()的使用
来看个案例:使用两个线程打印 1-100。线程1, 线程2 交替打印
public class PrintAlternately {
public static void main(String[] args) {
print print = new print();
Thread t1 = new Thread(print);
Thread t2 = new Thread(print);
t1.start();
t2.start();
}
}
class print implements Runnable {
private int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
//每次进来都唤醒所有线程,不然就死锁了
notify();
if (num <= 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + num);
num++;
try {
//让当前线程等待进入阻塞状态,并释放锁
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else break;
}
}
}
}
Thread-0=1
Thread-1=2
Thread-0=3
Thread-1=4
......
Thread-0=97
Thread-1=98
Thread-0=99
Thread-1=100
这个我之前有写错,逻辑没搞清楚,分享下2个错误写法,为什么错误。
public void run() {
while (num <= 2) {
synchronized (this) {
notify();
if (num <= 2) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + num);
num++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else break;
}
System.out.println(Thread.currentThread().getName() + "while里=" + num);
}
System.out.println(Thread.currentThread().getName() + "while外=" + num);
}
为了方便查看,把打印到100全都调到2
第一种错误写法,把 while (true)改成while (num <= 2)
- 会出现以下结果,发现程序还没停止,因为改成这样
- 打印完Thread-1=2,线程Thread-1被阻塞了
- 这时切换到Thread-0,走出if,打印while里,然后回到while循环发现num=3,不满足循环,跳出while,打印while外,Thread-0生命结束
- 会发现Thread-1还被阻塞,竟然没有被唤起,因为进不到synchronized代码块执行唤醒notify(),所以没打印了
所以,while (true)就是为了,下一个线程再进来,把所有线程唤醒,这只有俩线程所以用notify(),如果多个要改成notifyAll()。
第二种错误写法,是改成ture了,但是没有break语句
public void run() {
while (true) {
synchronized (this) {
notify();
if (num <= 2) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + num);
num++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
发现程序没退出,为啥呢
- 因为陷入死循环,没有跳出while循环语句,所以是该把打印的打印完了,但是却还在while里面一直循环,如果在if语句外面写个输出语句会发现会一直输出。
所以,我发现多线程代码,真的基础逻辑要好,不然真的死定了,要是在真实生产环境,陷入死循环,估计内存爆满,直接炸。
sleep与wait区别
相同点
- 一 旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点
- 两个方法声明的位置不同: Thread类中声明sLeep(),object类 中声明wait()
- 调用的要求不同: sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
- 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait() 会释放锁。
生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
- 生产者比消费者快时,消费者会漏掉一些数据没有取到。
- 消费者比生产者快时,消费者会取相同的数据。
分析:
- 无识别结果程问题?是,生产者线程,消费者线程
- 是否有共享数据?是,店员(或产品)
- 如何解决线程的安全问题?同步机制,有三种方法
- 是否涉及线程的通信?是
package producer_consumer_question;
//店员
class Clerk {
//商品
private int goods = 0;
public synchronized void productionGoods() {
if (goods < 20) {
goods++;
System.out.println(Thread.currentThread().getName() + "号生产者开始生产第" + goods + "件商品");
notifyAll();
}else {
try {
//大于等于停止生产。线程停止
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consumerGoods() {
if (goods > 0) {
System.out.println(Thread.currentThread().getName() + "号消费者开始消费第" + goods + "件商品");
goods--;
//唤醒生产者或消费者线程
notifyAll();
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer implements Runnable {
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.productionGoods();
}
}
}
class Consumer implements Runnable {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumerGoods();
}
}
}
public class Test {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Consumer consumer = new Consumer(clerk);
Thread p1 = new Thread(producer);
Thread p2 = new Thread(producer);
Thread c1 = new Thread(consumer);
Thread c2 = new Thread(consumer);
p1.setName("生产者1");
p2.setName("生产者2");
c1.setName("消费者1");
c2.setName("消费者2");
p1.start();
p2.start();
c1.start();
c2.start();
}
}
生产者2号生产者开始生产第1件商品
消费者1号消费者开始消费第1件商品
生产者1号生产者开始生产第1件商品
消费者2号消费者开始消费第1件商品
生产者2号生产者开始生产第1件商品
生产者1号生产者开始生产第2件商品
消费者2号消费者开始消费第2件商品
消费者1号消费者开始消费第1件商品
......
创建线程的第三种方式:实现Callable接口
通过实现Callable接口的call方法,可以返回异步计算的结果,结果由FutureTask来接受
以下是,通过两个线程来计算1到100的值,我很奇怪为什么两个线程的值会相同,不是有一个线程会提前结束吗,感觉不好理解。
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//3.创建Callable接口实现类的对象
Cal cal = new Cal();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask 的对象
FutureTask<Integer> f1 = new FutureTask<Integer>(cal);
FutureTask<Integer> f2 = new FutureTask<Integer>(cal);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象, 并调用start()
new Thread(f1).start();
new Thread(f2).start();
a.setName("A线程");
b.setName("B线程");
a.start();
b.start();
Integer sum1 = f1.get();
Integer sum2 = f2.get();
System.out.println("sum1:"+sum1);
System.out.println("sum2:"+sum2);
}
}
//1.创建一个实现Callable的实现类
class Cal implements Callable {
private int num = 1;
private int sum = 0;
//2.实现call方法,将此线程需要执行的操作声明在call中
@Override
public Object call() throws Exception {
for (; ; ) {
synchronized (this) {
if (num <= 100) {
System.out.println(Thread.currentThread().getName() + "=" + num);
sum += num++;
} else break;
}
}
return sum;
}
}
......
B线程=95
A线程=96
A线程=97
B线程=98
A线程=99
B线程=100
sum1:5050
sum2:5050
Callable/Future原理理解
一定先看这个https://www.jianshu.com/p/4974e445c6d6,在看我的讲解,就会很清楚,这篇文章讲解Callable/Future源码解析基本很清晰了,再加上自己动手看源码,基本就懂了。
里面涉及到Unsafe,看这俩篇https://www.jianshu.com/p/2e5b92d0962e,https://www.jianshu.com/p/db8dce09232d或者再看看别的
自己的理解:
我们需要知道的各类之间关系,UML图如下
整个大概执行过程 :
- 我们自定义的cal类通过实现Callable的call方法,写出自己的逻辑,和要返回的值
- 然后在使用的时候通过FutureTask类的带参---cal,构造初始化一个FutureTask
- 通过Thread带参--futureTask,实例化一个Thread启动线程
- 然后上面执行完,通过futureTask.get获取异步计算的结果
结论:实现Callable最后也要通过Thread类来启动线程,那FutureTask怎么获取计算结果的呢?
大致过程
- FutureTask是继承Runable,主要还是通过执行run()方法,所以start()方法一执行,就会先执行run()
- 在run()方法里调用Callable的call()方法,得到结果
- 然后把结果给FutureTask的自带属性outcome
- 在通过get()方法获取到这个outcome。
注意:get 方法是阻塞获取线程执行结果,当在main中调用f1.get(),这时其实是在等待另一个线程----FutureTask线程计算好结果。
向线程池中提交任务的submit方法不是阻塞方法,而Future.get方法是一个阻塞方法,当submit提交多个任务时,只有所有任务都完成后,才能使用get按照任务的提交顺序得到返回结果
我们来看下FutureTask主要的几个关键属性方法,怎么获取结果的过程,在代码里标好顺序了。
public class FutureTask<V> implements RunnableFuture<V> {
//返回结果或抛出异常的结果,就是call要返回的值
private Object outcome;
//1.start执行,run就会执行
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
//2.构造函数new FutureTask<Integer>(cal)里面的cal会赋值给c
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
//3.执行自定义的call(),并获取返回结果
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
//4.如果异常就把异常设置给结果
setException(ex);
}
if (ran)
//4.如果正常就把result设置给outcome
set(result);
}
} finally {
runner = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
//设置outcome的方法
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
//5.把v就是call的结果赋值给outcome
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
//get方法,返回计算后的结果
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
//6.程序调用get获取结果,就调用report()获取outcome
return report(s);
}
//获取计算好的结果
private V report(int s) throws ExecutionException {
//7.结果已经有值了,赋给x
Object x = outcome;
if (s == NORMAL)
//8.最后把结果x返回调用者get
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
}