线程
线程
基于线程安全的单例模式
双重双重检查锁(推荐使用)
public class Test1 {
//使用volatile关键字保其可见性
volatile private static Test1 instance = null;
private Test1() {
}
public static Test1 getInstance() {
try {
if (instance != null) {//懒汉式
} else {
//创建实例之前可能会有一些准备性的耗时工作
Thread.sleep(10);
synchronized (Test1.class) {
if (instance == null) {//二次检查
instance = new Test1();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}
synchronized和Lock锁的区别?
1.存在层次上,synchronized是Java的关键字,在jvm层面上;而Lock是一个接口
2.锁的释放上,synchronized是 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁;
而Lock是在finally中必须释放锁(lock.unlock()),不然容易造成线程死锁
3.锁的获取上,synchronized是假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待;
而Lock是尝试获得锁,线程可以不用一直等待
4.锁状态上,synchronized是无法判断;而Lock是可以判断
5.锁类型上,synchronized是可重入 不可中断 非公平;
而Lock是可重入 可判断 可公平(两者皆可) - 默认使用ReentrantLock无参构造方法,则创建非公平锁
6.性能上,synchronized是尽量同步少量代码;
而Lock是可以同步大量代码
7.目前上synchronized已经逐步优化,并且是JVM"亲生的"
锁
可重入锁:在执行对象中所有同步方法不用再次获得锁
可中断锁:在等待获取锁过程中可中断
公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写
wait和sleep的区别?
共同点: 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
不同点:
1.Thread.sleep(long)可以不在synchronized的块下调用,而且使用Thread.sleep()不会丢失当前线程对任何对象的同步锁(monitor);
2.0object.wait(long)必须在synchronized的块下来使用,调用了之后失去对object的monitor(同步锁), 这样做的好处是它不影响其它的线程对object进行操作。(不用synchronized会抛异常IllegalMonitorStateException)
3.sleep()是个静态方法 wait()非静态方法,属于Object
execute()和submit()区别
1.两者皆是执行线程任务
2.submit() ExecutorService中的; execute() Executor中 ExecutorService extends Executor
3.void execute(Runnable command);
Future submit(Callable task); Future<?> submit(Runnable task);
execute方法只能执行Runnable的任务,并且没有返回值;
submit方法既可以执行Runnable任务返回null,也能执行Callable任务有返回值;
4.submit方便Exception处理,可以通过捕获Future中的get()抛出的异常;
Callable的call()任务里可以抛出异常,而又希望外面的调用者能够感知这些exception并做出及时的处理,
例如有很多更新各种数据的任务task,希望如果其中一个task失败,其它的task就不需要执行了,
那就需要catch Future.get抛出的异常ExecutionException ,然后终止其它task的执行。
线程池
线程池工作原理
合理利用线程池能够带来三个好处
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
线程池的作用
线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果; 少了浪费了系统资源,多了造成系统拥挤效率不高。
用线程池控制线程数量,其他线程排队等候。
一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待任务,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
为什么要用线程池
1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。Java里面线程池的顶级接口是Executors,严格意义上讲Executors并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口ExecutorService。
比较重要的几个类
ExecutorService 真正的线程池接口。 ScheduledExecutorService
能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。 ThreadPoolExecutor
ExecutorService的默认实现。 ScheduledThreadPoolExecutor
继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。
- newSingleThreadExecutor 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
- newFixedThreadPool 创建固定大小的线程池。 每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束, 那么线程池会补充一个新线程。- newCachedThreadPool 创建一个可缓存的线程池。 如果线程池的大小超过了处理任务所需要的线程, 那么会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,
此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,
线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。- newScheduledThreadPool 创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
死锁
什么是死锁
在多线程程序中,使用了多把锁,造成线程之间相互等待.程序不往下走了。
产生死锁的条件
死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个进程使用;
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系;
线程三大特性
可见性
解决线程间变量的不可见性的方案有两种常见方式
1.加锁
解决原因:会清空工作内存,读取主内存中最新值到工作内存中来。
2.对共享的变量进行volatile关键字修饰
解决原因:一旦一个线程中的变量,添加了volatile修饰符,其它线程可以立即读取到最新值
volatile与synchronized的区别
1).volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
2).volatile保证数据的可见性,但是不保证原子性(即:多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制,实现线程安全。
3).从性能上说,volatile更好点,仅仅是对实现线程间变量的可见性上。
原子性
概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
问题原理说明
public class VolatileAtomicThread1 implements Runnable {
// 定义一个int类型的遍历
private int count = 0 ;
@Override
public void run() {
// 对该变量进行++操作,100次
for(int x = 1 ; x <= 100 ; x++) {
count++ ;
System.out.println("count =========>>>> " + count);
}
}
}
public class VolatileAtomicThreadDemo1 {
public static void main(String[] args) {
// 创建VolatileAtomicThread对象
VolatileAtomicThread1 volatileAtomicThread = new VolatileAtomicThread1() ;
// 开启100个线程对count进行++操作
for(int x = 1 ; x <= 100 ; x++) {
new Thread(volatileAtomicThread).start();
}
}
}
执行结果:不保证一定是10000
以上问题主要是发生在count++操作上:
count++操作包含3个步骤:
- 从主内存中读取数据到工作内存
- 对工作内存中的数据进行++操作
- 将工作内存中的数据写回到主内存
count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。
1)假设此时x的值是100,线程A需要对改变量进行自增1的操作,首先它需要从主内存中读取变量x的值。由于CPU的切换关系,此时CPU的执行权被切换到了B线程。A线程就处于就绪状态,B线程处于运行状态。
2)线程B也需要从主内存中读取x变量的值,由于线程A没有对x值做任何修改因此此时B读取到的数据还是100。
3)线程B工作内存中x执行了+1操作,但是未刷新到主内存中。
4)此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内存中的变量值还是100,没有失效。A线程对工作内存中的数据进行了+1操作。
5)线程B将101写入到主内存。
6)线程A将101写入到主内存 虽然计算了2次,但是只对A进行了1次修改。
有序性
重排序就是编译器或者CPU的代码的的结构重排排序,已达到最佳的执行效果。重排大概分为编译器重排,处理器重排。
三特性总结
原子性:在一次或者多次操作时,要么所有操作都被执行,要么所有操作都不执行。
可见性:当一个线程对共享变量进行修改后,另外一个线程可以立即看到该变量修改后的最新值。
有序性:程序执行的顺序按照代码的先后顺序执行。