并发和工具类总结
线程8大核心基础知识
实现多线程的方法
两种实现方法:
1.实现Runnable接口
/**
* 用Runnable方式创建线程
*/
public class RunnableStyle implements Runnable{
public static void main(String[] args) {
Thread thread = new Thread(new RunnableStyle());
thread.start();
}
@Override
public void run() {
System.out.println("用Runnable方法实现线程");
}
}
2.继承Thread类
/**
* 用Thread类方式实现线程
*/
public class ThreadStyle extends Thread{
public static void main(String[] args) {
new ThreadStyle().start();
}
@Override
public void run() {
System.out.println("用Thread类方式实现线程");
}
}
其中用Runnable接口实现更好,原因为:
1.我们的run方法实现应该与Thread类是解耦的,使用Thread类是耦合在一起的。
2.继承Thread类,每次创建都需要创建这个子类,并且之后还要进行销毁等操作,如果用Runnable接口,那么可以使用线程池等进行管理,减少资源的消耗
3.继承Thread类以后,那么就无法继承其他的类,那就减少了类的可扩展性。
两种方法本质对比:
方法一:最终调用target.run()
方法二:run()方法整个直接重写
总结:通常可以分为两类,Oracle官方文档是这么写的,准确的说,创建线程只有一种方式,那就是构造Thread类,而实现线程的执行单元有两种方式:
1.实现Runnable接口的run方法,并把Runnable实例传给Thread类,这时执行的start方法,会调用target.run()方法
2.重写Thread的run方法,也就是继承Thread类,这时候会直接执行重写的方法。
其他的实现方式都是通过包装类,或者其他方式,其本质都是通过上述两种方式进行实现,并不能说明是新的实现线程执行单元的方式。
启动线程的方式
1.start()方法
调用start方法,会告诉JVM要启动新线程,而启动新线程是由主线程去执行start方法,所以牵扯到两个线程。
新的线程会等待CPU资源,进入就绪状态,同一Thread不能执行两次start方法
start()源码中,会先检查启动新线程的线程状态,然后加入到线程组中,之后调用start0(),这里面检查状态为:
java线程初始状态为not ye started,代表的threadStatus就是0,那么在进行start的时候,会判断这个状态是否为0,也就是否为初始状态,如果它不是初始状态了,那么就会抛出异常,所以start方法不能执行两次。
2.run()方法
run方法的实现为:
所以说run()方法本身他是没有创建新的线程的,而是调用的本线程,也就是主线程,所以执行出来的就是主线程来执行,这也是为什么一般情况下都用start方法来创建线程,因为run()方法本身没有创建新线程的功能。
停止线程的方式
正确的停止线程
使用interrupt来通知,而不是强制,就像关闭电脑一样,通知这个线程停止,那么被停止的线程可能会进行收尾工作后,在停止,但是决定权是在这个线程上,这个线程也可以决定不停止,由这个线程的逻辑决定。
所以正确的停止线程是由通知线程停止,和被通知的线程如何配合来完成停止才是停止线程的核心。不能鲁莽的使用stop方法进行强制停止,因为有些线程处理的方法是很重要的,要等到它们执行完毕,或者他们不想停止也是ok的,那么就保证了数据的安全和完整性,也让线程完成了结束后的清理工作等等。
线程停止的情况:run()方法执行完毕或者出现异常情况。
通常情况下停止线程:
/**
* run方法内没有sleep或wait方法时,停止线程
*/
public class RightWayStopThreadWithoutSleep implements Runnable{
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
@Override
public void run() {
int num = 0;
// 要用Thread.currentThread().isInterrupted()来判断是否通知停止状态,如果不写,那么线程会完全执行到底后才会停止
while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
if (num % 10000 == 0) {
System.out.println(num + "是10000的倍数");
}
num++;
}
System.out.println("任务执行结束");
}
}
2.阻塞的情况下进行停止的情况
先列举阻塞情况下,使用interrupt无法正常停止的情况:
/**
* 带有sleep的中断线程的写法
*/
public class RightWayStopThreadWithSleep {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
while (num <= 300 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务结束");
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(500);
thread.interrupt();
}
}
/**
* 如果在执行过程中,每次循环都会调用sleep或wait方法
* 那么我们不需要每次迭代时候都检查是否被通知停止
*/
public class RightWayStopThreadSleepEveryLoop {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num <= 10000) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务结束");
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
/**
* 在循环里面进行捕获,但是由于sleep的实现方式,sleep执行的时候会把interrupt的标志位清除,所以判断无效
*/
public class CantInterput {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
while (num <= 10000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("任务结束");
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
那么实际开发中解决阻塞中断的最佳实践为:
原则是不应屏蔽中断
1.优先选择:传递中断
在run()方法中通过catch住异常后,进行处理,使线程停止或保存日志等等。
不能在子方法中,因为如果在子方法进行try/catch,在不知道线程处理的情况下,往往无法正确的进行处理,且不同的线程可能实现的内容不同,都调用一个方法,但是用不同的异常捕获逻辑。
/**
* 最佳实践:catch了InterruptException之后的优先选择:在方法签名中抛出异常
* 那么在run()方法就会强制try/catch
*/
public class RightWayStopThreadInprod implements Runnable{
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("go");
try {
throwInMethod();
} catch (InterruptedException e) {
// 保存日志,停止程序
System.out.println("保存日志,停止程序");
e.printStackTrace();
}
}
}
private void throwInMethod() throws InterruptedException {
Thread.sleep(1000);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInprod());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
2.不想或无法传递:恢复中断
如果在子方法catch住了,那么也要务必重新设置中断,让调用此方法的线程可以进行自己的逻辑处理。
/**
* 最佳实践2:在catch子语句中调用Thread.currentThread().interrupt()来恢复设置中断状态,
* 以便于在后续的执行中,依然能够检查到刚才发生了中断
* 回到刚才RightWayStopThreadInprod补上中断,让它跳出
*/
public class RightWayStopThreadInprod2 implements Runnable{
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("程序运行结束");
break;
}
System.out.println("go");
reInMethod();
}
}
private void reInMethod() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInprod2());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
响应中断的方法总结:
错误的停止方法:
1.已经被弃用的stop,suspend和resume方法。
stop:
/**
* 用stop()来停止线程,会导致线程运行一半突然停止,
* 没办法完成一个基本单位的操作,会造成脏数据
*/
public class StopThread implements Runnable{
@Override
public void run() {
// 模拟指挥军队:一共有5个连队,每个连队10人,发放武器弹药,叫到号的士兵去领取
for (int i = 0; i < 5; i++) {
System.out.println("连队" + (i + 1) + "开始领取武器");
for (int j = 0; j < 10; j++) {
System.out.println(j + 1);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("连队" + (i + 1) + "已经领取完毕");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new StopThread());
thread.start();
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.stop();
}
}
suspend和resume方法会挂起这个线程并且带着锁,那么在其他地方调用这个线程所执行的方法时候,就很容易造成死锁,就需要主动唤醒。
2.用volatile设置boolean标记位
代码演示:
BlockingQueue可以理解为满的时候再放入元素或空的时候取出元素,都会进行阻塞。
/**
* 当陷入阻塞时,volatile是无法停止线程的
* 此例中,生产者的生产速度很快,消费者消费速度慢
* 就会出现阻塞队列满了,生产者就会阻塞,等待消费者进一步消费
*/
public class WrongWayVolatileCantStop {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
Producer producer = new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);
Consumer consumer = new Consumer(storage);
while (consumer.needMoreNums()) {
System.out.println(consumer.storage.take() + "被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了");
// 一旦消费者不需要更多数据了,那么就应该让生产者停下来,但是并没有停止,没有输出生产者结束
producer.canceled = true;
System.out.println(producer.canceled);
}
}
class Producer implements Runnable {
public volatile boolean canceled = false;
BlockingQueue storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled) {
if (num % 100 == 0) {
storage.put(num);
System.out.println(num + "是100的倍数,被放到仓库中");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者停止运行");
}
}
}
class Consumer{
BlockingQueue storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.95) {
return false;
}
return true;
}
}
所以上面出现这种情况的原因就是在storage.put()方法中被阻塞了,没办法在while处判断,所以线程一直没有完全停止。但是interrupt方法可以响应这些阻塞的方法,抛出interrupt异常让我们去捕获,进行接下来的逻辑处理,所以这也是为什么interrupt是正确的停止线程的方法。
正确的修改方式:
/**
* 对WrongWayVolatileCantStop进行修正,使用interrupt进行停止通知
*/
public class WrongWayVolatileFixedp {
public static void main(String[] args) throws InterruptedException {
WrongWayVolatileFixedp body = new WrongWayVolatileFixedp();
ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
Producer producer = body.new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);
Consumer consumer = body.new Consumer(storage);
while (consumer.needMoreNums()) {
System.out.println(consumer.storage.take() + "被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了");
producerThread.interrupt();
}
class Producer implements Runnable {
BlockingQueue storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
storage.put(num);
System.out.println(num + "是100的倍数,被放到仓库中");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者停止运行");
}
}
}
class Consumer{
BlockingQueue storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.95) {
return false;
}
return true;
}
}
}
interrupt源码:
判断是否已被中断相关方法:
1.static boolean interrupted():返回当前线程状态,并将中断状态设置为false
2.boolean isInterrupted():查看调用线程状态
第一个方法是静态方法,它会返回线程状态之后,把中断状态设置为false,要注意的是,第一个方法的目标,对象是当前线程,而不管本方法来自于哪个对象,举个例子就是,我在main方法中创建了一个线程newThread,然后newThread.interrupted(),那么这时候设置的目标就是main线程,也就是主线程,并不是newThread线程,因为是在main方法中调用的,那么newThread的停止状态还是不会改变,改变的是main线程的状态。
线程的生命周期
1.New
线程已经被创建了,但是还没有运行,处于准备就绪的状态。
2.Runnable
从New到调用start方法,运行线程时候的状态。
Runnable表示可运行的,并不一定要运行中才是Runnable,等待CPU分配执行时间也是Runnable。
3.Blocked
被synchronized修饰的代码块,被其他线程拿走拥有权,那么就会进入Blocked堵塞。
但是Blocked只会用于synchronized修饰的,其他锁的情况堵塞并不是Blocked。
4.Waiting
线程进入等待的状态,调用Object.wait()等方法时候进入,那么在唤醒的时候会重回Rnnable状态
5.Timed Waiting
与Waiting类似,区别在于进入等待时候要有时间限制,比如sleep方法,也可以提前唤醒。
6.Terminated
线程执行完毕就会终止,或者出现异常也会终止。
代码演示:
/**
* 展示线程的New,Runnable,Terminated状态,即使是正在运行,也是Runnable状态
*/
public class NewRunnableTerminated implements Runnable{
public static void main(String[] args) {
Thread thread = new Thread(new NewRunnableTerminated());
// 打印出NEW状态
System.out.println(thread.getState());
thread.start();
// 打印出Runnable状态
System.out.println(thread.getState());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印出Runnable状态
System.out.println(thread.getState());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印出Terminated状态
System.out.println(thread.getState());
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(i);
}
}
}
/**
* 展示Blocked,Waiting,TimedWaiting
*/
public class BlockedWaitingTimedWaiting implements Runnable{
public static void main(String[] args) throws InterruptedException {
BlockedWaitingTimedWaiting runnable = new BlockedWaitingTimedWaiting();
Thread thread1 = new Thread(runnable);
thread1.start();
Thread thread2 = new Thread(runnable);
thread2.start();
Thread.sleep(5);
// 打印出Timed_Waiting状态,因为正在执行Thread.sleep(1000)
System.out.println(thread1.getState());
// 打印出Blocked状态,因为thread2想拿到syn的锁,却拿不到
System.out.println(thread2.getState());
Thread.sleep(2000);
// 这时候thread1已经执行到了wait()方法,那么就会进入到Waiting状态
System.out.println(thread1.getState());
}
@Override
public void run() {
syn();
}
private synchronized void syn() {
try {
Thread.sleep(1000);
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
阻塞状态,一般而言,把Blocked,Waiting,Timed_Waiting都称为阻塞状态,不仅仅是Blocked。
Thread和Object类中的线程相关方法
方法概览:
wait,notify,notifyAll方法
1.wait方法会让线程进入阻塞阶段
直到以下情况之一,才会被唤醒:
1.另一个线程调用这个对象的notify()方法,并且刚好被唤醒的就是本线程
2.另一个线程调用这个对象的notifyAll()方法
3.过了wait(long timeout)规定的超时时间,如果传入0就是永久等待。
4.线程自身调用了interrupt()
2.唤醒阶段
notify方法会唤醒单个等待需要被唤醒的线程,如果有多个,那么就会选择任意一个线程进行唤醒。
且wait和notify都需要在synchronized方法块里执行,如果在外面执行,会抛出异常。
notifyAll区别在于,会把所有等待被唤醒的线程,都进行唤醒。
3.遇到中断
如果阻塞情况下被中断了,也就是抛出interruptException,那么就会释放掉已经获取到的monitor
代码演示:
/**
* 展示wait和notify的基本用法
* 1.研究代码执行顺序: 线程1先执行到wait后,进入阻塞状态,然后释放了锁,
* 线程2拿到了锁之后执行notify,对线程1进行唤醒,然后继续执行后面,
* 那么线程1被唤醒之后,也继续执行下面的代码
* 2.证明wait释放锁
*/
public class Wait {
public static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
thread1.start();
Thread.sleep(100);
thread2.start();
}
static class Thread1 extends Thread {
@Override
public void run() {
synchronized (object) {
System.out.println("线程1开始执行了");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1获取到了锁");
}
}
}
static class Thread2 extends Thread {
@Override
public void run() {
synchronized (object) {
System.out.println("线程2开始执行了");
object.notify();
System.out.println("线程2调用了notify");
}
}
}
}
/**
* 3个线程,线程1和线程2首先被阻塞,线程3唤醒他们,分别用notify和notifyAll
* start先执行不代表线程先启动
*/
public class WaitNotifyAll implements Runnable{
private static final Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new WaitNotifyAll();
Thread threadA = new Thread(runnable);
Thread threadB = new Thread(runnable);
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
resourceA.notifyAll();
//resourceA.notify();
System.out.println("ThreadC notified");
}
}
});
threadA.start();
threadB.start();
//Thread.sleep(200);
threadC.start();
}
@Override
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName() + "got resourceA lock");
try {
System.out.println(Thread.currentThread().getName() + "wait to start");
resourceA.wait();
System.out.println(Thread.currentThread().getName() + "is waiting to end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 证明wait只释放当前的那把锁
*/
public class WaitNotifyReleaseOwnMonitor{
public static Object resourceA = new Object();
public static Object resourceB = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println("ThreadA got resourceA lock");
synchronized (resourceB) {
System.out.println("ThreadA got resourceB lock");
try {
System.out.println("ThreadA release resourceA lock");
resourceA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceA) {
System.out.println("ThreadB got resourceA lock");
System.out.println("ThreadB try to resourceB lock");
synchronized (resourceB) {
System.out.println("ThreadB got resourceB lock");
}
}
}
}).start();
}
}
wait原理:
生产者消费者模式
生产者消费者模式:由于生产者和消费者的生产和消费的速度不同,所以为了更好的提高效率,生产者把消息放入到队列中使用,而消费者则是从队列中取出消息来进行消费,当生产者发现队列满的时候就休息,然后消费者发现队列空的时候就唤醒生产者进行消息的生产,与消息队列的结构类似。
代码演示:
/**
* 用wait/notify来实现生产者消费者模型
*/
public class ProducerConsumerModel {
public static void main(String[] args) {
EventStorage storage = new EventStorage();
Producer producer = new Producer(storage);
Consumer consumer = new Consumer(storage);
new Thread(producer).start();
new Thread(consumer).start();
}
}
class Producer implements Runnable {
private EventStorage storage;
public Producer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.put();
}
}
}
class Consumer implements Runnable {
private EventStorage storage;
public Consumer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.take();
}
}
}
class EventStorage {
private int maxSize;
private LinkedList<Date> storage;
public EventStorage () {
maxSize = 10;
storage = new LinkedList<>();
}
public synchronized void put() {
while (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("仓库里有了" + storage.size() + "个产品");
notify();
}
public synchronized void take() {
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("拿到了" + storage.poll() + ",现在仓库还剩下" + storage.size());
notify();
}
sleep方法
作用:只想让线程在预期的时间执行,其他时候不要占用CPU资源
sleep方法不释放锁,包括synchronized和lock,和wait不同
代码演示:
/**
* 展示线程sleep的时候不释放synchronized的monitor,等sleep时间到了以后,正常结束后才释放锁
*/
public class SleepDontReleaseMonitor implements Runnable{
public static void main(String[] args) throws InterruptedException {
SleepDontReleaseMonitor runnable = new SleepDontReleaseMonitor();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
Thread.sleep(10);
thread2.start();
}
@Override
public void run() {
syc();
}
private synchronized void syc() {
System.out.println("线程" + Thread.currentThread().getName() + "获得到了monitor");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "退出了同步代码块");
}
}
/**
* sleep不是放lock(lock需要手动释放)
*/
public class SleepDontReleaseLock implements Runnable{
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
SleepDontReleaseLock runnable = new SleepDontReleaseLock();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
Thread.sleep(10);
thread2.start();
}
@Override
public void run() {
try {
lock.lock();
System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁");
Thread.sleep(5000);
System.out.println("线程" + Thread.currentThread().getName() + "已苏醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
};
}
}
sleep方法响应中断:
1.抛出InterruptException
2.清除中断状态(第二种写法更优雅)
代码演示:
/**
* 每隔1秒输出当前时间,被中断,观察
* Thread.sleep()
* TimeUnit.SECONDS.sleep()
*/
public class SleepInterrupted implements Runnable{
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new SleepInterrupted());
thread.start();
Thread.sleep(6500);
thread.interrupt();
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(new Date());
try {
TimeUnit.MINUTES.sleep(1);
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println("我被中断了!");
e.printStackTrace();
}
}
}
}
总结:sleep方法可以让线程进入Waiting状态,并且不占用CPU资源,但是不释放锁,直到规定时间后再执行,休眠期间如果被中断,会抛出异常并清除中断状态。
wait/notify , sleep异同:
1.相同点:
都会进入阻塞状态,且都可以响应中断
2.不同点:
wait/notify必须再同步方法中执行,而sleep不需要
wait/notify会释放锁,而sleep不需要
sleep必须要传入参数,wait/notify可以不用
sleep所属Thread类,wait/notify属于Object类
join方法
作用:因为新的线程加入了我们,所以我们要等他执行完再出发
普通用法:
/**
* 演示join,注意语句输出顺序,会变化。
*/
public class Join {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
});
thread1.start();
thread2.start();
System.out.println("开始等待子线程运行完毕");
thread1.join();
thread2.join();
System.out.println("所有子线程执行完毕");
}
}
遇到中断:
/**
* 演示join期间被中断的效果
*/
public class JoinInterrupt {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
thread.interrupt();
Thread.sleep(5000);
System.out.println("Thread1 finished");
} catch (InterruptedException e) {
System.out.println("子线程中断");
e.printStackTrace();
}
}
});
thread1.start();
System.out.println("开始等待子线程运行完毕");
try {
thread1.join();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "主线程被中断了");
thread1.interrupt();
e.printStackTrace();
}
}
}
在被join期间,线程处于Waiting状态。
CountDownLatch或CyclicBarrier是可以实现出join的功能,因为java本身已经有这种很优秀的类,那么尽量使用这些类,出错的概率就会更低。
join源码:
这也是为什么jdk的join底层里调用的是wait,但是没有notify,join结束后还会被唤醒的原因,因为Thread在运行结束之后,会进行类似notify的操作,所以一般都是对象进行wait,而不是线程进行wait的原因。
join的等价写法:
根据源码可知,Thread类的wait,最后会进行notify的操作,那么就相当于join的操作了。
/**
* 通过讲解join原理,分析出join的代替写法
*/
public class JoinPrinciple {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
});
thread1.start();
thread2.start();
System.out.println("开始等待子线程运行完毕");
synchronized (thread1) {
thread1.wait();
}
synchronized (thread2) {
thread2.wait();
}
/*thread1.join();
thread2.join();*/
System.out.println("所有子线程执行完毕");
}
yield方法
作用:释放我的CPU时间片,但是线程状态还是Runnable,并不是释放锁,且JVM不保证遵循
yield只是暂时的将自己的调度权让给别人,但是随时可能再次被调度
线程的属性
属性名称 | 用途 |
---|---|
编号(ID) | 每个线程有自己的ID,用于标识不同的线程 |
名称(Name) | 作用让用户或程序员在开发,调试或运行过程中,更容易区分每个不同的线程,定位问题等 |
是否是守护线程(isDaemon) | true代表该线程是守护线程,false代表线程是非守护线程,也就是用户线程 |
优先级(Priority) | 优先级这个属性的目的是告诉线程调度器,用户希望哪些线程相对多运行,哪些少运行 |
1.线程ID
线程id由于在源码中初始就进行++threadId,所以创建的线程并不是从0开始
2.线程名
对于没有线程名字的线程,用一下默认名字,且Number++,Number从0开始。
而设置线程名字的源码为:
通过setName可以更改java层的name,但是一旦线程启动起来,那么name已经用setNativeName方法进行,在JVM底层的名字是没有办法更改的。
3.守护线程
作用:给用户线程提供服务
那么在用户线程都结束的情况下,守护线程没有要守护的线程对象了,那么就会和JVM一起关闭。
3个特性:线程类型默认继承自父线程,被谁启动(通常是由JVM来启动),不影响JVM退出(并不看守护线程运不运行,而是看用户线程还有没有)
与用户线程的区别:整体无区别,唯一区别在于JVM的离开,守护线程不会影响,用户线程没有则会影响JVM结束。
我们可以设置守护线程,但是不应该设置守护线程,如果将用户线程设置为守护线程,那么如果这个守护线程正在进行数据的更新,结果其他用户线程都结束了,那么JVM判断没有用户线程了,就会关闭JVM,那么这个守护线程更新的过程就会终止,那么很可能会导致数据的不一致或者丢失。
4.优先级
10个级别,默认5,子线程也是5。
我们的程序设计不应依赖优先级,因为不同操作系统对于优先级的理解是不一样的,我们设置10,但是操作系统中并不一定是10,比如windows最高是7,并不是一一对应,会随着操作系统变化,那么程序是不可靠的,所以一般都是用默认就可以
线程异常
主线程可以轻松发现异常,但是子线程不行,子线程异常无法用传统方法来捕获(try catch),因为在主线程内捕获子线程的异常对于主线程来说并不是异常,而是子线程发生的异常,try catch只能捕获本线程发生的异常。
所以就需要UncaughtExceptionHandler来全局捕获异常,对于子线程不能直接捕获的异常进行全局捕获,提高健壮性。虽然可以在每个run方法里进行try catch 但是线程很多的情况下,这么做无疑增加了代码的复杂度,且每个不同的run实现方法都要进行捕获。
异常处理器的调用策略:
先查看是否有这个异常是否有父类,如果有则递归调用,如果没有,那么查看有没有设置的异常处理器,如果有,就执行异常处理器里面的逻辑,如果没有则输出系统线程异常错误
实现方式:
1.给程序统一设置
2.给每个线程单独设置
3.给线程池设置
主要是对方法1进行实现,代码演示如下:
/**
* 自己定义的全局异常捕获器
*/
public class MyUncaughtExceeptionHandler implements Thread.UncaughtExceptionHandler {
private String name;
public MyUncaughtExceeptionHandler(String name) {
this.name = name;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.WARNING, "糟糕了,线程" + t.getName() + "崩了", e);
System.out.println("捕获器" + name + "已捕获到线程" + t.getName() + "的错误,异常为:" + e);
}
}
/**
* 使用自己的异常捕获器
*/
public class UseOwnUncaughtExceptionHandler implements Runnable{
public static void main(String[] args) throws InterruptedException {
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceeptionHandler("1号完整版"));
new Thread(new UseOwnUncaughtExceptionHandler()).start();
Thread.sleep(1000);
new Thread(new UseOwnUncaughtExceptionHandler()).start();
Thread.sleep(1000);
new Thread(new UseOwnUncaughtExceptionHandler()).start();
Thread.sleep(1000);
new Thread(new UseOwnUncaughtExceptionHandler()).start();
}
@Override
public void run() {
throw new RuntimeException();
}
}
线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和胶体执行,也不需要进行额外的同步或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以得到正确的结果,那这个对象是线程安全的。
线程不安全:get同时set,额外同步
如果全部都线程安全,那么运行速度降低或代码开发的难度增加,设计成本也会增大
以下几种情况会出现线程安全问题:
1.运行结果错误:a++多线程下出现消失的请求现象
2.活跃性问题:死锁,活锁,饥饿
3.对象发布和初始化的时候的安全问题
index++导致的线程不安全
运行结果错误:
那么为了解决这个错误,可以用等待线程的方式,在index++前设置一道门槛,之后再设置一道门槛,那么正确的情况就是index + 2,错误的情况就是 + 1,然后判断标记位和前一个标记为是否都是true,如果都是,那么表示只加了1,运行结果是错误的,代码如下:
public class MutiThreadError implements Runnable{
private static MutiThreadError runnable = new MutiThreadError();
private static Integer index = 0;
private static AtomicInteger realIndex = new AtomicInteger();
private static AtomicInteger errorIndex = new AtomicInteger();
private static boolean[] marked = new boolean[100000];
private static volatile CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
private static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("表面上:" + index + ", 实际上:" + realIndex + ",错误次数;" + errorIndex);
}
@Override
public void run() {
marked[0] = true;
for (int i = 0; i < 10000; i++) {
cyclicBarrier.reset();
try {
cyclicBarrier2.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
// 这时候正确的情况是两个线程同时++
// 错误的情况是一个线程在进行+1之后赋予给index前
// 切换到线程2,那么线程2读取到的还是原来的index
// 结果两个线程同时进行+1,那么index本身还是赋予了+1
index++;
realIndex.incrementAndGet();
try {
cyclicBarrier2.reset();
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
synchronized (runnable) {
if (marked[index] && marked[index - 1]) {
System.out.println("发生错误" + index);
errorIndex.incrementAndGet();
}
marked[index] = true;
}
}
}
}
对象发布和初始化
发布:使对象能够被当前范围之外的代码所看见。比如通过类的非私有方法返回对象的引用。
逸出:如果一个类还没有构造结束就已经提供给了外部代码一个对象引用即发布了该对象,此时叫做对象逸出,对象的逸出会破坏线程的安全性。
逸出的情况:
1.方法返回一个private对象(本意是不让外部访问)
2.还未完成初始化(构造函数没全执行完毕)就把对象提供给外界,比如:
1).在构造函数中未初始化完毕就this赋值
2).隐式逸出——注册监听事件
3).构造函数中运行线程
解决方式:1.用副本来代替private对象,代码如下:
/**
* 解决第一种方法返回一个private对象,这里面可以用副本来代替
*/
public class MutiThreadError1 {
public static void main(String[] args) {
Demo demo = new Demo(new HashMap<>());
System.out.println(demo.getMap().get("1"));
demo.getMap().put("1", null);
System.out.println(demo.getMap().get("1"));
}
}
class Demo {
private Map<String, String> map;
public Demo(Map<String, String> map) {
map.put("1", "周一");
map.put("2", "周一");
map.put("3", "周一");
map.put("4", "周一");
this.map = map;
}
public Map<String, String> getMap() {
// 直接返回map会让外部的类直接对map进行修改
// return this.map
// 所以创建一个副本来返回
return new HashMap<>(this.map);
}
}
解决方法二还未完成初始化(构造函数没全执行完毕)就把对象提供给外界:
/**
* 描述: 用工厂模式修复刚才的初始化问题
*/
public class MultiThreadsError7 {
int count;
private EventListener listener;
private MultiThreadsError7(MySource source) {
listener = new EventListener() {
@Override
public void onEvent(MultiThreadsError5.Event e) {
System.out.println("\n我得到的数字是" + count);
}
};
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 100;
}
public static MultiThreadsError7 getInstance(MySource source) {
MultiThreadsError7 safeListener = new MultiThreadsError7(source);
source.registerListener(safeListener.listener);
return safeListener;
}
public static void main(String[] args) {
MySource mySource = new MySource();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new MultiThreadsError5.Event() {
});
}
}).start();
MultiThreadsError7 multiThreadsError7 = new MultiThreadsError7(mySource);
}
static class MySource {
private EventListener listener;
void registerListener(EventListener eventListener) {
this.listener = eventListener;
}
void eventCome(MultiThreadsError5.Event e) {
if (listener != null) {
listener.onEvent(e);
} else {
System.out.println("还未初始化完毕");
}
}
}
interface EventListener {
void onEvent(MultiThreadsError5.Event e);
}
interface Event {
}
}
总结
各种需要考虑线程安全的情况:
多线程带来的性能问题:
Java内存模型
由于JVM在不同的CPU平台的机器指令不同,那么就会无法保证 并发安全的效果一致,那么就需要一个转化过程的规范,原则来保证每个平台的并发安全的效果是一致的。
JMM是一种规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。
JMM还是工具类和关键字的原理,没有JMM,那么就需要指定什么时候用内存栅栏等,非常麻烦,有了JMM,只需要使用同步工具和关键字就可以开发程序了。
指令重排序
看下列代码:
/**
* 描述: 演示重排序的现象 “直到达到某个条件才停止”,测试小概率事件
*/
public class OutOfOrderExecution {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
CountDownLatch latch = new CountDownLatch(3);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
two.start();
one.start();
latch.countDown();
one.join();
two.join();
String result = "第" + i + "次(" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.out.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
在此代码中,除了多线程可能会出现的三种情况外,还出现了一种情况,就是x = 0,y = 0,照理说,按程序乖乖执行的话是不可能出现这种情况的,这种情况发生的情况就是指令重排序。
程序中原本的顺序可能变成了,先执行y = a,a = 1,x = b,b = 1的情况,正好使x,y都赋予了a,b原来的值,也就是0。
重排序:在线程内部的两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,他们的顺序被改变了,这就是重排序。
重排序的好处:1.对指令进行优化,提高处理速度。
在图中,节省了a的重新载入和重新写入,减少了指令的次数,且结果是不变的,那么自燃就提高了处理速度。
重排序的三种情况:
1.编译器优化:包括JVM,JIT编译器等
2.CPU指令重排:就算编译器不发生重排,CPU也可能对指令进行重排
3.内存的“重排序”:这里的重排序并不是真正的重排序,而是由于内存中有缓存的存在,那么就会导致更新了缓存后,没有向内存更新,那么下一个线程访问主存的时候就会没有看到之前的线程的修改,表面上像是进行了重排序,这也是引出可见性的问题。
可见性
代码演示:
/**
* 描述: 演示可见性带来的问题
*/
public class FieldVisibility {
volatile int a = 1;
volatile int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b=" + b + ";a=" + a);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
以上代码一般情况下会出现:1.a = 3,b = 2
2.a = 1,b = 2
3.a = 3,b = 3
这三种情况,但是还会出现一种特殊情况就是a = 1,b = 3,也就是线程2看到了b更新后的值,但是没有看到a的值,这就是可见性的问题。也就是如下的情况,线程1和线程2都将数据更新到了自己的核的缓存中,但是不一定能及时更新到主内存中去,或共有缓存L3 Cache中,那么就会出现数据上可见性的差异。
那么解决这个问题,就可以用到volatile关键字,volatile关键字的作用是将修饰的变量的值,强制刷回到内存中,那么其他的线程在读取内存的值时候,就一定是已经修改后的值了。
为什么会有可见性的问题:
1.高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间多了缓存层,也就是CPU Cache层
2.线程间的对于共享变量的可见性问题不是直接由多核引起的,而是多缓存引起的。也就是CPU有多级缓存,导致读的数据过期
3.如果每个核都只用一个缓存,那么也就不存在内存可见性问题了,但是每个核心都会将自己需要的数据读到独占缓存中,数据修改后写入到缓存,然后等待刷入主存,那么就会导致有些核心的值是一个过期的值。
JMM的抽象:主内存和本地内存
JMM抽象了主内存和本地内存的概念,是对于寄存器,一级缓存,二级缓存等的抽象。
主内存就是L3 Cache和主内存,本地内存就是每个核到L2 Cache。
** JMM有以下规定:
1.所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝
2.线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中。
3.主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成
所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题
happens-before原则
happens-before规则就是用来解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A,这就是happens-before。
那么执行顺序就是:如果一个操作happe-before于另一个操作,那么说第一个操作对于第二个操作是可见的。
happens-before规则有哪些:
1.单线程原则
在一个线程之内的,那么前面的代码一定是执行于后面代码前的。无论是否发生重排序,真正执行前面的代码永远会被执行在后面的代码所看到。
2.锁操作(synchronized和Lock)
3.volatile变量
只要完成了写操作,那么就一定会得到更新后的值
4.线程启动
新启动的线程一定会看到前面的线程所执行的操作。
5.线程join
主线程要等待所有线程执行完毕,那么就一定会看到这些线程执行后的操作
6.传递性
如果A = B,并且B = C,那么就可以推出 A = C
7.中断
一个线程被其他线程interrupt时,那么检测中断(isInterrupt)或者抛出InterruptedException一定能看到
8.工具类的Happens-Before原则
1.线程安全的容器get一定能看到在此之前的put等存入操作
2.CountDownLatch
3.Semaphore
4.Future
5.线程池
6.CyclicBarrier
案例:
这里给b加了volatile,不仅b被影响,也可以实现轻量级同步,b之前的写入,对去读b后的代码都可见,所以在对a的赋值,其他线程读取a的时候一定也是可见的,所以这里的a即使不加volatile,只要b读到是3,就可以由happens-before原则保证了读到的都是3,而不可能读取到1.
对synchronized可见性的理解:
synchronized不仅保证了原子性,还保证了可见性。
synchronized不仅让被保护的代码安全,且synchronized和之前的所有代码也会被看到
原子性
原子性:一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。
Java里的原子操作:
1.除long和double之外的基本类型的赋值操作
2.所有引用类型的赋值操作
3.java.concurrent.Atomic.*包中所有类的原子操作
long和double是64位的,但是由于32位系统每次写入32位,那么就会导致这两种类型的前32位是原子的,但是后32位还不是原子的操作的情况,如果Java虚拟机是运行在64位系统就可以正常保持原子性,如果不确定的情况下,可以为64位值进行volatile声明。但是实际开发中,并不用担心这个问题,商用虚拟机中已经把这种类型的写入过程保证了原子性。
由上面的情况也可以看出,两个原子操作组合在一起并不能保证整体依然具有原子性,即使全同步的HashMap也不完全安全。
单例模式
为什么需要单例模式:1.节省内存和计算:获取一次,全程通用
2.保证结果正确:单例模式下保证了数据的原子性
3.方便管理:一次实例化即可。
单例模式适用场景:
1.无状态工具类,比如日志工具类,不管是在哪使用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,那么这时候只需要一个实例对象即可。
2.全局信息类:比如在一个类上记录网站的访问次数。
单例模式下8种写法:
1.饿汉式(静态常亮)(可用)
/**
* 饿汉式写法(静态常量)
*/
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return INSTANCE;
}
}
2.饿汉式(静态代码块)
/**
* 饿汉式(静态代码块)
*/
public class Singleton2 {
private final static Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getInstance() {
return INSTANCE;
}
}
3.懒汉式(线程不安全)(不可用)
/**
* 懒汉式(线程不安全)
*/
public class Singleton3 {
private static Singleton3 INSTANCE;
private Singleton3() {
}
// 如果这里有多个线程同时进来的情况
public static Singleton3 getInstance() {
// 那么有可能都认为INSTANCE为null,进行多次实例化,
// 那么就不符合单例模式的要求,所以是线程不安全的
if (null == INSTANCE) {
INSTANCE = new Singleton3();
}
return INSTANCE;
}
}
4.懒汉式(线程安全,同步方法)(不推荐用)
/**
* 懒汉式(线程安全,同步方法)(不推荐用)
*/
public class Singleton4 {
private static Singleton4 INSTANCE;
private Singleton4() {
}
// 如果这里有多个线程同时进来的情况
// 虽然线程安全了,但是消耗的资源更多了
// 不仅要增加锁等待,还要进行上下文切换等消耗
public synchronized static Singleton4 getInstance() {
if (null == INSTANCE) {
INSTANCE = new Singleton4();
}
return INSTANCE;
}
}
5.懒汉式(线程不安全,同步方法)(不可用)
/**
* 懒汉式(线程不安全,同步方法)(不能使用)
*/
public class Singleton5 {
private static Singleton5 INSTANCE;
private Singleton5() {
}
// 如果这里有多个线程同时进来的情况
public synchronized static Singleton5 getInstance() {
// 虽然在if里面增加了synchronized块
// 但是进方法的时候并不会上锁,也就是其他线程还是会进来
// 那么等待synchronized释放锁之后还会new新的Singleton5
// 不仅要增加锁等待,还要进行上下文切换等消耗,且线程还是不安全的
if (null == INSTANCE) {
synchronized (Singleton5.class) {
INSTANCE = new Singleton5();
}
}
return INSTANCE;
}
}
6.双重检查(面试用)
/**
* 双重检查
*/
public class Singleton6 {
private volatile static Singleton6 INSTANCE;
private Singleton6() {
}
// 如果这里有多个线程同时进来的情况
public synchronized static Singleton6 getInstance() {
// 在if里面增加了synchronized块
if (null == INSTANCE) {
// 那么由于线程还是会进到这里
synchronized (Singleton6.class) {
// 那么我们可以在synchronized代码块里再进行一层判断
// synchronized里肯定只有一个线程,那么第一个创建实例化后
// 后面的线程拿到这个锁进到这里时发现已经实例化,那么就不会再次实例化了
if (null == INSTANCE) {
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
优点:线程安全,延迟加载,效率较高
这里面要用volatile关键字,因为新建对象是有三个步骤构成:
1.先创建一个空的对象
2.调用构造方法
3.再把创建好的实例赋值到引用上
那么这里面还是会有小概率事件发生重排序的问题,所以要用volatile禁止重排序,否则可能只把空对象给了第一个线程,然后后面所有的线程都用空的对象,并没有走构造方法
7.静态内部类(推荐用)
/**
* 静态内部类
*/
public class Singleton7 {
private Singleton7() {
}
private static class SingletonInstance {
private final static Singleton7 INSTANCE = new Singleton7();
}
// 因为是在这个类内部的方法
// 所以只有在调用getInstance()的时候,才会对INSTANCE进行实例化
// 那么相较于饿汗式,在不使用的情况下效率就会高一些,减少资源的不必要的浪费
public static Singleton7 getInstance() {
return SingletonInstance.INSTANCE;
}
}
8.枚举(推荐用,最佳用法)
/**
* 静态内部类
*/
public enum Singleton8 {
INSTANCE;
public void wache() {
}
}
最简洁的单例实现模式。
各写法对比:
饿汉:简单,但是没有懒加载,有时会浪费内存
懒汉:一定程度上写的不好情况下会造成线程安全问题
静态内部类:一定程度避免了线程安全,也进行了懒加载,可用
双重检查:面试用(因为相对比较复杂,考察范围比较广)
枚举:最推荐,好处有:
1.写法简单
2.线程安全有保障,枚举会被编译成final class,并且会集成Emnu这个父类,这个父类的方法都是用static修饰的
3.第一次使用的时候才会进行实例化,也是一种懒加载
4.避免反序列化破坏单例,比如通过反射可以绕过private,但是枚举不可以,所以更好的提供了安全性。
各种写法的适用场景:
1.最好的就是利用枚举
2.非线程安全的方法不能使用
3.如果程序一开始要加载的资源太多,那么就应该使用懒加载
4.饿汉式如果对象的创建需要配置文件,那么就不适用
5.懒加载虽然好,但是静态内部类这种方式会引入编程复杂性
volatile
volatile是一种同步机制,比synchronized或者Lock县官更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。
如果一个变量被修饰称volatile,那么JVM就直到这个变量可能会被并发修改
相应的开销小,能力也有限,volatile做不到像synchronized那样的原子保护,只能保证并发情况下的可见性和禁止重排序。
作用:1.可见性:读一个volatile变量之前,需要先使响应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存
2.禁止指令重排序优化:解决单例双重乱序问题
适用场景
不适用于a++的情况,因为如果要保证a的值是正确的,是需要相关的原子性操作,但是volatile只能保证修改后
其他线程可以看到,并不能保证修改的过程不会发生资源争抢。
适用场合1:一个共享变量自始自终只被各个线程赋值,而没有其他的操作,也就是没有相关联的读取数据后,在此数据基础上的操作,那么就可以用volatile来保证可见性,因为赋值本身是有原子性的,那么就可以保证线程安全。
2.作为刷新之前变量的触发器
像上面的情况,a = 3就一定会被其他线程所看到,因为b修饰成volatile,并且执行于a后面,这里面b = 0或者b = 1等,无论对b进行怎么的修改,前面的操作也一定会被看到,那么b这一行的操作就相当于一个触发器,来触发了前面可见的操作。
总结
1.volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,或作为触发器,实现轻量级同步。
2.volatile属性的读写操作都是无锁的,不能代替synchronized,因为它没有提供原子性和互斥性,但是也因为无锁,不需要花费时间在获取锁和释放锁上面,所以说是低成本的。
3.volatile只能作用于属性,修饰的属性就不会做指令重排序。
4.volat提供了可见性,热河一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主存中读取
5.volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作
6.volatile可以使得long和double的赋值是原子的。
Synchronized
作用是能够保证在同一时刻最多只有一个线程执行该段代码,以保证并发安全的效果。是最基本的互斥同步手段,是Java的关键字,被Java语言原生支持。
如果不使用并发手段进行a++,最后结果会比预计少,因为a++他实际包含三个操作:
1.读取a;2.将a加一;3.将a的值写入到内存中
那么在并发的时候,可能会存在a加一,但是没写入到内存中,但是另一个线程执行的时候,读取到的还是没加的a,所以就还是原来的值加一,那么写入到内存的值和前一个线程写入的值一样,那么就会有一个值被吞掉了。
用法
1.对象锁
包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)
/**
* 对象锁实例1,代码块形式
*/
public class Sync1 implements Runnable{
Object lock1 = new Object();
Object lock2 = new Object();
@Override
public void run() {
// 锁不同锁住的情况就不同,如果执行完代码块,就会释放这个对象
synchronized (lock1) {
System.out.println("我是lock1,我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "lock1运行结束");
}
synchronized (lock2) {
System.out.println("我是lock2,我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "lock2运行结束");
}
}
public static void main(String[] args) {
Sync1 sync = new Sync1();
Thread t1 = new Thread(sync);
Thread t2 = new Thread(sync);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
/**
* 对象锁实例,方发锁形式
*/
public class Sync2 implements Runnable{
@Override
public void run() {
try {
method();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void method() throws InterruptedException {
System.out.println("我是对象锁的方法修饰符形式,我叫" + Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
Sync2 sync = new Sync2();
Thread t1 = new Thread(sync);
Thread t2 = new Thread(sync);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
2.类锁
指synchronized修饰静态的方法或指定锁为Class对象,Java类可能有很多个对象,但只有一个Class对象。所谓的类锁,不过是Class对象的锁,类锁只能在同一时刻被一个对象拥有,就算是不同的Runnable,也只能有一个类锁。
形式1:synchronized加在static方法上
形式2:synchronized(*.class)代码块
/**
* 类锁的第一种形式, static形式
*/
public class Sync3 implements Runnable{
@Override
public void run() {
try {
method();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void method() throws InterruptedException {
System.out.println("我是类锁的第一种形式,static形式,我叫" + Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
Sync3 sync = new Sync3();
Sync3 sync2 = new Sync3();
Thread t1 = new Thread(sync);
Thread t2 = new Thread(sync2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
/**
* 类锁的第二种形式,.class形式
*/
public class Sync4 implements Runnable{
@Override
public void run() {
try {
method();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method() throws InterruptedException {
synchronized (Sync4.class) {
System.out.println("我是类锁的第二种形式,.class形式,我叫" + Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}
public static void main(String[] args) {
Sync4 sync = new Sync4();
Sync4 sync2 = new Sync4();
Thread t1 = new Thread(sync);
Thread t2 = new Thread(sync2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
七种常见问题
1.两个线程同时访问一个对象的同步方法
那么会按照顺序执行。
2.两个线程访问的是两个对象的同步方法
那么synchronized相当于不起作用,因为访问的是不同的对象
3.两个线程访问的是synchronized的静态方法
因为方法是静态的,所以会按照锁对象顺序执行
4.同时访问同步方法与非同步的方法
那么同步的方法会按照顺序执行,但是非同步的方法并不会影响其并发行为。
5.访问同一对象的不同普通同步方法
还是会串行顺序执行。
6.同时访问静态synchronized和非静态synchronized方法
那么会同时进行,因为静态synchronized会锁住所有的线程执行此方法,但是另一个线程执行的是另外的非静态synchronized方法,所以会同步执行。
7.方法抛异常后,会释放锁
七种情况的总结:
1.一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1,5种情况)
2.每个实例都对应有自己的一把锁,不同实例之间互不影响;例外:锁对象是*.class以及静态synchronized修饰的时候,所有对象共用一把类锁(对应2,3,4,6种情况)
3.无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应第7种情况)
性质
1.可重入
可重入是指同一线程的外层函数获得锁之后,内层函数可以直接在此获取该锁,好处是避免死锁,提升封装性
粒度:线程而非调用
2.不可中断
一旦这个锁已经被别人获得了,那么就只能等待,直到别的线程释放这个锁,如果别的线程永远不释放锁,那么就要永远地等待下去。
相比Lock,可以拥有中断的能力,如果觉得等的时间太长,有权中断现在已经获取到锁的线程的执行,如果等待太长时间不想等,就可以退出。
缺点
效率低:锁的释放情况少,不能中断一个正在试图获得锁的线程。
不够灵活:加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
无法知道是否成功获取到锁
调试方法
死锁
死锁是发生在并发中的,当两个或多个线程或进程之间,相互持有对方所需要的资源,又不主动 释放,导致所有人都无法继续前进,那么就会导致程序陷入无尽的阻塞,这就是死锁。
多个线程之间的依赖关系是一个环形,存在环路的锁的依赖关系,那么就会发生死锁。
死锁的影响在不同系统中是不一样的,这取决于系统对死锁的处理能力,比如数据库中,检测出来死锁,那么就会放弃一个事务,让另一个事务可以执行,但是在JVM中,无法自动处理,因为JVM无法判断哪个线程更重要,无法自行决定,但是JVM有检测的工具,帮助我们来排查死锁。
死锁发生的几率不高,但是危害特别大,一旦发生,多是高并发场景,那么影响的用户数量是很多的,很可能造成整个系统崩溃,子系统崩溃,导致性能降低,而且压测的时候无法找出所有潜在的死锁。
死锁发生的4个必要条件:
1.互斥条件
2.请求与保持条件
3.不剥夺条件
4.循环等待条件
缺一不可
发生的情况案例:
1.最简单的情况:
/**
* 最简单的死锁情况
*/
public class SimpleDeath implements Runnable{
int flag = 1;
// 注意这里一定要用static
static Object o1 = new Object();
static Object o2 = new Object();
public static void main(String[] args) {
SimpleDeath runnable1 = new SimpleDeath();
SimpleDeath runnable2 = new SimpleDeath();
runnable1.flag = 0;
runnable2.flag = 1;
new Thread(runnable1).start();
new Thread(runnable2).start();
}
@Override
public void run() {
if (flag == 1) {
synchronized (o1) {
System.out.println("线程" + Thread.currentThread().getName() + "拿到了o1锁");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("线程" + Thread.currentThread().getName() + "拿到了o2锁");
}
}
}
if (flag == 0) {
synchronized (o2) {
System.out.println("线程" + Thread.currentThread().getName() + "拿到了o2锁");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("线程" + Thread.currentThread().getName() + "拿到了o1锁");
}
}
}
}
}
分析:首先当类的对象flag =1时,先锁定o1,睡眠500毫秒,然后锁定o2;
而在前一个线程睡眠的时候,另一个flag=0的对象线程启动,先锁定o2,睡眠500毫秒,等待前面释放o1;
第一个线程睡眠结束后需要锁定o2才能继续,而此时o2已经被第二个线程锁定;
同样道理,第二个线程睡眠结束要锁定o1,但是o1已经被第一个线程锁定;
两个线程互相等待,都需要对方锁定的资源才能继续执行,从而发生死锁。
注意看退出信号(不是0的都是不正常退出的信号,要避免不正常退出):
2.哲学家就餐问题
伪代码:
但是由于每个哲学家都只拿了左边的刀叉,要拿起右边的时候,发现已经被别人拿了,就会一直处于等待状态,那么就会发生死锁。
定位和修复死锁的方法
1.jstack -PID
2.ThreadMXBean代码演示
修复死锁的方法:
**保存案发现场,保存堆栈信息,然后立刻重启服务器
1.避免策略
思路:避免相反的获取锁的顺序,不在乎获取锁的顺序,通过hashcode来决定获取锁的顺序,但是一般由于数据库里都有主键,所以通过主键更容易来实现
/**
* 描述: 转账时候遇到死锁,但是可以通过HashCode来避免
*/
public class TransferMoney implements Runnable {
int flag = 1;
static Account a = new Account(500);
static Account b = new Account(500);
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
TransferMoney r1 = new TransferMoney();
TransferMoney r2 = new TransferMoney();
r1.flag = 1;
r2.flag = 0;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("a的余额" + a.balance);
System.out.println("b的余额" + b.balance);
}
@Override
public void run() {
if (flag == 1) {
transferMoney(a, b, 200);
}
if (flag == 0) {
transferMoney(b, a, 200);
}
}
public static void transferMoney(Account from, Account to, int amount) {
class Helper {
public void transfer() {
if (from.balance - amount < 0) {
System.out.println("余额不足,转账失败。");
return;
}
from.balance -= amount;
to.balance = to.balance + amount;
System.out.println("成功转账" + amount + "元");
}
}
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash < toHash) {
synchronized (from) {
synchronized (to) {
new Helper().transfer();
}
}
}
else if (fromHash > toHash) {
synchronized (to) {
synchronized (from) {
new Helper().transfer();
}
}
}else {
synchronized (lock) {
synchronized (to) {
synchronized (from) {
new Helper().transfer();
}
}
}
}
}
static class Account {
public Account(int balance) {
this.balance = balance;
}
int balance;
}
}
关于哲学家就餐问题,解决方法有四种:
1).服务员检查(避免策略)
2).改变一个哲学家拿叉子的顺序(避免策略),代码演示:
/**
* 描述: 解决哲学家就餐问题导致的死锁
*/
public class DiningPhilosophers {
public static class Philosopher implements Runnable {
private Object leftChopstick;
public Philosopher(Object leftChopstick, Object rightChopstick) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
}
private Object rightChopstick;
@Override
public void run() {
try {
while (true) {
doAction("Thinking");
synchronized (leftChopstick) {
doAction("Picked up left chopstick");
synchronized (rightChopstick) {
doAction("Picked up right chopstick - eating");
doAction("Put down right chopstick");
}
doAction("Put down left chopstick");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
Thread.sleep((long) (Math.random() * 10));
}
}
public static void main(String[] args) {
Philosopher[] philosophers = new Philosopher[5];
Object[] chopsticks = new Object[philosophers.length];
for (int i = 0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
for (int i = 0; i < philosophers.length; i++) {
Object leftChopstick = chopsticks[i];
Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
// 这里最后一个哲学家没有按照原来的方向拿
// 而是相反方向,那么就不会形成环路,也就不会发生死锁
if (i == philosophers.length - 1) {
philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
} else {
philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
}
new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
}
}
}
3)餐票(避免策略)
4)领导调节(检测与恢复策略)(如果发生死锁,那么就会定期使一个哲学家释放资源)
2.检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一资源,来打开死锁
这个策略允许发生死锁,每次调用锁都记录,然后定期检查“锁的调用链路图”中是否存在环路,一旦发生死锁,就用死锁恢复机制进行恢复
恢复方法1:进程终止,逐个终止线程,直到死锁消除,按照重要程度,占用资源情况和运行时间来安排终止顺序
恢复方法2:资源抢占,将已经分发出去的锁给回收回来,让线程回退几步,这样就不用结束整个线程,但是有可能同一个线程一直被抢占,就造成其他线程得不到运行
3.鸵鸟策略:因为发生死锁的概率极其低,那么我们就可以直接忽略它,直到死锁发生的时候,再人工修复
避免死锁的方式:
1.设置超时时间
Lock的tryLock(long timeouot, TimeUnit unit)
synchronized不具备尝试锁的能力,会一直等待
代码演示:
/**
* 描述: 用tryLock来避免死锁
*/
public class TryLockDeadlock implements Runnable {
int flag = 1;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
TryLockDeadlock r1 = new TryLockDeadlock();
TryLockDeadlock r2 = new TryLockDeadlock();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (flag == 1) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
System.out.println("线程1获取到了锁1");
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
System.out.println("线程1获取到了锁2");
System.out.println("线程1成功获取到了两把锁");
lock2.unlock();
lock1.unlock();
break;
} else {
System.out.println("线程1尝试获取锁2失败,已重试");
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程1获取锁1失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag == 0) {
try {
if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
System.out.println("线程2获取到了锁2");
Thread.sleep(new Random().nextInt(1000));
if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {
System.out.println("线程2获取到了锁1");
System.out.println("线程2成功获取到了两把锁");
lock1.unlock();
lock2.unlock();
break;
} else {
System.out.println("线程2尝试获取锁1失败,已重试");
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程2获取锁2失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
2.多使用并发类而不是自己设计锁
ConcurrentHashMap,ConcurrentLinkedQueue,AtomicBoolean等
实际应用中java.util.concurrent.atomic十分有用,简单方便,且效率比Lock更高
3.尽量降低锁的使用粒度,用不同的锁而不是一个锁
4.如果能使用同步代码块,就不使用同步方法:自己指定锁对象
5.给线程起有意义的名字,debug和排查时事半功倍,框架和JDK都遵守这个最佳实践
6.避免锁的嵌套:MustDeadLock类
7.分配资源前先看能不能收回来
8.尽量不要几个功能用同一把锁:专锁专用
活锁
活锁和死锁的区别在于,活锁是重复执行没有意义的循环,导致程序得不到进展,举个例子就是,哲学家问题中,所有哲学家同时拿起左边的餐叉,之后等待5分钟,同时又放下手中的餐叉,之后等5分钟再次拿起。
虽然线程并没有阻塞,也始终在运行,但是程序本身得不到进展,因为线程始终重复执行同样的流程,比起死锁,活锁不仅不容易检测,且一直在运行消耗资源
那么解决活锁的情况可以加入随机性,导致不会同时发生,如果在消息队列中一直重试处理失败,那么就可以将这个消息放在队列后面,以至于不会一直影响其他消息的处理,然后对这个消息进行重试次数的限制,然后到达限制后,存储到数据库中,然后通过定时任务从数据库中取出这些超过重试次数的消息,再次重试,并且可以启用报警机制,提醒后台人员消息出现问题的警告。
饥饿
当线程需要某些资源(例如CPU),却始终得不到
比如线程的优先级设置过低,或者有某线程持有锁同时又无限循环从而不释放锁,或者某程序始终占用某文件的写锁
饥饿会导致响应性变差,如果后台线程把所有的资源占用了,那么前台线程将无法得到很好的执行,对于用户来说,体验就会很差
面试题
有多少种创建线程的方式?
首先从不同的角度看,会有很多种,但是典型的答案就是两种,分别是Runnable接口和继承Thread类,且Oracle官方解释也是两种。
但是两种方法的本质是一样的,都是通过run方法的内容来源,只不过Runnavble接口的方式,会判断target是否为null,然后执行target.run(),继承Thread类则是重写整个run()方法。
其他的还有的方法,比如创建线程池,虽然也能新建线程,但是源码里还是实现Runnable接口和继承Thread类。
对于两种方法来说,更偏向使用Runnabale接口的方式,原因有三个:
1.从代码架构角度上,我们具体要实现的事情是run()方法,而线程的创建,销毁,运行等生命周期相关的事情是Thread类去实现的,两个的实现目的不同,所以我们要进行解耦。
2.从新建线程的损耗来说,如果继承Thread类,那么每次创建新的线程,都要重新对继承Thread类的子类进行创建,而用Runnable接口的方式,就可以利用线程池,反复利用同一个线程,那么对于生命周期的损耗是减少的。
3.从Java不支持双继承的角度来说,如果用继承Thread类,那么对于这个子类来说,大大限制了它的扩展性,无法继承其他的类。
如果一个线程调用两次start()方法会出现什么情况,为什么
如果两次调用start方法会抛出异常,因为在源码中会先检查启动新线程的线程状态,然后加入到线程组中,之后调用start0(),这里面检查状态为:
java线程初始状态为not ye started,代表的threadStatus就是0,那么在进行start的时候,会判断这个状态是否为0,也就是否为初始状态,如果它不是初始状态了,那么就会抛出异常。
为什么不用run()方法,而是用start方法呢
调用start方法,会告诉JVM要启动新线程,而启动新线程是由主线程去执行start方法,但是run()方法本身他是没有创建新的线程的,而是调用的本线程,也就是主线程,所以执行出来的就是主线程来执行。
如何停止线程
用interrupt来请求,好处是:
通知线程停止,和被通知的线程如何配合来完成停止才是停止线程的核心。不能鲁莽的使用stop方法进行强制停止,因为有些线程处理的方法是很重要的,要等到它们执行完毕,或者他们不想停止也是ok的,那么就保证了数据的安全和完整性,也让线程完成了结束后的清理工作等等。
但是想停止线程,首先在请求方要使用interrupt方法,被停止方则要在适当的时候或者循环的时候处理intterrupt抛出的异常,如果是子方法被调用,那么在子方法中要抛出相应的线程异常或处理的时候,对interrupt重新设置。
不用stop方法是因为可能会造成数据的缺失,线程没有正常结束等,suspend和resume方法则会挂起这个线程并且带着锁,那么在其他地方调用这个线程所执行的方法时候,就很容易造成死锁,就需要主动唤醒。
volatile的boolean无法处理长时间的阻塞,比如BlockQueue put的时候因为满了而堵塞,那么这时候用boolean进行判断是无法结束这个堵塞的。
无法响应中断时如何停止线程
这要根据不同的类调用不同的方法,如果线程阻塞因为调用了wait,sleep或join等方法,可以通过中断线程,抛出InterruptException来唤醒该线程,但是对于不能响应InterruptException的堵塞,没有通用的解决方法。
但是可以利用特定的方式来响应中断的方法:
比如ReentrantLock.lockInterruptibly(),比如关闭套接字使线程立即返回等方法来达到目的。
有很多原因会造成线程阻塞,那么根据不同的情况,唤醒的方法也不同。
线程有哪几种状态,线程的生命周期
用程序实现两个线程交替打印0 ~ 100的奇偶数
/**
* 两个线程交替打印0 ~ 100 的奇偶数,用synchronized关键字实现
*/
public class WaitNotifyPrinOddEvenSync {
private static int count = 0;
private static final Object lock = new Object();
// 新建2个线程
// 1个只处理偶数,第二个只处理奇数(用位运算)
// 用synchronized来通信
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (count < 100) {
synchronized (lock) {
if ((count & 1) == 0) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
}
}
}
}
}, "偶数").start();
new Thread(new Runnable() {
@Override
public void run() {
while (count < 100) {
synchronized (lock) {
if ((count & 1) == 1) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
}
}
}
}
}, "奇数").start();
}
}
/**
* 两个线程交替打印0 ~ 100的奇偶数,用wait和notify
*/
public class WaitNotifyPrinOddEvenWait {
private static int count = 0;
private static final Object lock = new Object();
// 1.拿到锁,就打印
// 2.打印完,唤醒另一个线程,自己就休眠
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
while (count <= 100) {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
lock.notify();
if (count <= 100) {
try {
// 如果任务还没结束,就让出当前的锁,就让给其他的线程
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}, "偶数").start();
Thread.sleep(50);
new Thread(new Runnable() {
@Override
public void run() {
while (count < 100) {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
lock.notify();
if (count < 100) {
try {
// 如果任务还没结束,就让出当前的锁,就让给其他的线程
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}, "奇数").start();
}
}
为什么wait()需要在同步代码块内使用,而sleep()不需要
主要是为了通信的可靠,防止死锁或永远等待的发生,如果没有synchronized保护,那么随时有可能会被切换到别的线程执行,如果这个线程要执行wait()方法,另一个线程执行notify()方法,结果先执行了notify()方法,导致切换到本线程执行wait()方法的时候,进入了永久等待或者死锁的发生,所以就把这种需要配合使用的方式,放到了synchronized代码块中使用。
sleep则是与本线程的执行相关,与其他线程没有相配合的关联,所以不需要synchronized保护。
为什么wait(),notify()和notifyAll()被定义在Object类里,而sleep定义在Thread类里
因为锁住的是根据某个对象,而不是锁住某个线程,线程是可以拥有多把锁的,这样定义就会增加线程的可用性,根据使用某个对象的锁进行等待或唤醒,而sleep则是只与本线程的等待相关,跟锁住的对象是无关的。