线程安全简单分析
成员变量和静态变量是否线程安全?
1.如果没有共享,线程安全
2.如果被共享,根据他们是否能够改变
如果只有读,线程安全
如果有读写操作,则这段代码是临界值,需要考虑线程安全
局部变量是否线程安全?
是安全的
但是局部变量引用未必安全
-
如果该对象没有逃出方法的作用访问,是线程安全的
-
如果该对象逃出方法的作用范围,需要考虑线程安全
分析i++线程是否安全?
结论:
很显然不是线程安全的
分析:
使用 javac xxxxxx.java
指令来编译
使用 javap -c xxxxx.class
指令来反编译刚才得到的字节码文件
分析i++的过程
0: aload_0
1: dup
2: getfield #2
5: iconst_1
6: iadd
7: putfield #2
10: return
- getfield:从内存中获取变量 count 的值
- iadd:将 count 加 1
- putfield:将加 1 后的结果赋值给 count 变量
可见i++不是由一条指令执行所,这也就是线程不安全的原因所在,因为 count++ 操作不具备原子性。
解决:
使用volatile 和 synchronized
volatile 在并发编程中保证了共享变量的 “可见性”,可见性的意思是当一个线程修改一个共享变量时,另外一个线程立即能读到这个修改的值。
使用 synchronized 进行加锁可以保证该计数器线程安全,因为只有一个线程能取得锁去执行 count++,其他线程必须等待锁的获取。
常见的线程安全的类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- concurrent包下面
这里所说的线程安全是多个线程同时使用一个实例的某个方法时,线程是安全的。也可以理解为
-
它们每个方法都是原子的
-
注意他们的很多个方法组合不是原子的
Hashtable table = new Hashtable();
//线程1 线程2
if(table.get("key") == null){
table.put("key",value);
}
线程是否安全案例
例1.
1.不安全 Map线程不安全
2.安全 线程安全类
3.安全 线程安全类
4.不安全 date不属于线程安全类
5.不安全 final只是保证了引用唯一,但是不能保证年月日的修改
例2
Servlet是单例的只有一份,则userService被多个线程共享,所以其中的count是被共享的。如果需要count++,需要对于临界代码块的保护
例3
这是一个用来记录一个方法执行多久,使用前后通知时间来得差值。很明显是线程不安全的如果不加@Scope默认都是单例的,既然单例则被共享,对象对于成员变量的修改就会出现问题。
解决办法,使用环绕通知,将开始和结束时间做成环绕通知的局部变量,如此保证线程安全
例4
此处都是线程安全的,Dao中没有成员变量,没有成员变量的类一般都是线程安全的,Connection属于局部变量,每个线程都创建在自己的栈内存。所以线程安全
ServiceImpl属于无状态,没有成员变量
UserService虽然有成员变量但是他是私有的,不可变所以我们注入的时候就需要使用private
例5
懒汉式,饿汉式哪个是线程安全的
线程活跃性
活锁
活锁出现在两个线程相互改变对方的结束条件,最后谁也无法结束
死锁
线程一拥有对象A的锁,线程二拥有对象B的锁,线程相互等待对方释放资源,此时线程阻塞出现假死状态,形成死锁。
饿锁
一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。多线程中优先级高的会优先执行,并且抢占优先级低的资源,导致优先级低的线程无法得到执行。
与死锁不同的是,饥饿锁在一段时间内,优先级低的线程最终还是会执行的,比如高优先级的线程执行完之后释放了资源。
ReentranLock与synchronize区别
-
可中断
-
可以设置超时时间
-
可以支持多个条件变量
都是可重入锁
可打断锁
使用interrupt打断
synchronize,ReentrantLock都是非公平锁。
ReentrantLock可以设置公平非公平,阻塞队列中顺序进行,一般不会设置成公平锁这样会降低并发。
ReentrantLock可以使用TryLock尝试获取锁。
应用方面
- 互斥:使用synchronized或者Lock达到共享资源互斥的效果
- 同步:使用wait/notify或者Lock条件变量来达到线程通信效果
线程池:
线程池状态ThreadPoolExecutor使用int的高3位来表示线程池状态,低29位表示线程数量
为什么要存储在一个原子变量中?目的是将线程池状态与线程个数合二为一,这样就可以用一次cas原子进行赋值
线程池参数
线程池刚开始是没有线程的,有任务后才会创建
救急线程创建条件:
必须选择有界队列,才会有救急线程
执行流程:
首先Task进入创建核心线程 -----核心线程满了
其次Task进入阻塞队列 -----阻塞队列满了
再次Task被救急线程处理 ----救急线程不够用
最后执行拒绝策略
如果救急线程使用结束后,过期时间是专门针对救急线程的。
newFixedThreadPool
- 核心线程数=最大线程数
- 使用的是无界队列
使用场景
任务量已知,比较耗时的任务
newCachedThreadPool
- 全部创建的都是救急线程,时间为60s后可以回收,无界队列
- 队列采用了SyncjronousQueue实现特点是,没有容量,没有线程来取是不放进去的
使用场景
适合任务数比较密集,但是每个任务时间比较短
newSignleThreadExecutor
使用场景
执行多个任务是串行的
newFixedThreadPool(1)newSignleThreadExecutor有什么区别
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止呢么没有任何的补救措施,而线程池会新建一个线程,保证其正常工作
- sign的线程数始终为1不能被改变。返回值不同,sign包装了一下返回,这样不可以调线程的其他方法
- fixed初始值为1之后是可以修改的,对外暴露ThreadPool对象,强转后调用setCorePoolSize方法修改
线程池提交任务
线程池创建多大合适
过小导致不能充分发挥系统资源,容易导致饥饿
过大导致线程上下文切换,占更多内存
cpu密集型运算
数据分析类 cpu+1个线程。保障操作系统的页确实故障
io密集型运算
Timer任务调度线程
简单易用但是都是由一个线程来调度,因此所有任务都是串行执行的,同一时间只有一个任务执行,有一个任务延时或者异常都会影响后面的任务
任务调度线程池ScheduledThreadPoll
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
pool.schedule(()->{
System.out.println("aaaa");
int a = 1/0;
},1, TimeUnit.SECONDS);
pool.schedule(()->{
System.out.println("bbb");
},1, TimeUnit.SECONDS);
}
解决了之前的问题,而且遇到了异常还能执行剩下的任务
循环执行
System.out.println("start");
pool.scheduleAtFixedRate(()->{
System.out.println("开始执行");
try {
sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
},1,1,TimeUnit.SECONDS);
四个参数分别是:执行方法体,第一次间隔多久执行,执行间隔,时间单位。
执行方法时只会间隔两秒
System.out.println("start");
pool.scheduleWithFixedDelay(()->{
System.out.println("开始执行");
try {
sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
},1,1,TimeUnit.SECONDS);
四个参数分别是:执行方法体,第一次间隔多久执行,执行间隔,时间单位
执行方法时会间隔三秒,以第一次执行结束时开始
如何捕捉异常
自己捕捉异常try catch。返回future对象当get()方法的时候就会将异常打印
指定时间执行
/**
* 每周四 14:35执行
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
long time = 1000 * 60 * 60 * 24 * 7;
System.out.println("now"+now);
// 时间修改为指定时间
LocalDateTime localDateTime = now.withHour(14).withMinute(35).withSecond(0).withNano(0).with(DayOfWeek.THURSDAY);
System.out.println("localDateTime"+localDateTime);
if(now.compareTo(localDateTime)>0){
// 如果周五则加一周
localDateTime.plusWeeks(1);
}
// 时间相减
long l = Duration.between(now, localDateTime).toMillis();
System.out.println("l"+l);
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
pool.scheduleAtFixedRate(()->{
System.out.println("执行");
},l,time,TimeUnit.MILLISECONDS);
}
AQS
localDateTime.plusWeeks(1);
}
// 时间相减
long l = Duration.between(now, localDateTime).toMillis();
System.out.println("l"+l);
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
pool.scheduleAtFixedRate(()->{
System.out.println("执行");
},l,time,TimeUnit.MILLISECONDS);
}
##### AQS