可以结合之前的博客内容观看Java多线程初识
为什么会在多线程下存在线程安全问题
随着时间的流逝,我们计算机硬件也在不断的迭代更新,CPU、内存、I/O设备这三者的速度存在差异,主要表现:
CPU增加了高速缓存,为了较为平衡与内存交互的速度
操作系统中的线程被CPU分时复用也是为了提高交互速度
代码在被编译成执行指令顺序是为了CPU更合理利用
以上其实都是硬件层面的优化,而程序最后享受着这些成果,但是线程安全确实这些优化方面造成的
例如:并发状态下多个线程被CPU来切换调度运行时间片,此时正好操作了一个共享变量,然后操作共享变量的指令多个情况下就会被切换,中断等操作。这种就体现了原子性问题,反之在操作一个或者多个在CPU中执行的指令不被中断的特性就可以称作原子性
线程安全的源头之一
原子性问题
直接上代码体现
static int i =0;//共享变量i
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 0; j < 1000; j++) {//执行1000次
i++;
}
});
Thread t2 = new Thread(()->{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 0; j < 1000; j++) {//执行1000次
i++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();//t1,t2 相互等待对方线程执行完成,没有完成时main线程阻塞
System.out.println(i);//执行结果 i < 2000
}
执行结果
1850
以上代码感官上运行结果正常是:2000
但是为什么 i < 2000呐,这个是由于两个线程并发执行同一块代码,操作了同一个共享变量,在CPU层面“i++”,被拆分成了多个执行指令
GETFIELD i : I // 访问变量i
ICONST_1 // 将整形常量1放入操作数栈
IADD // 把操作数栈中的常量1出栈并相加,将相加的结果放入操作数栈
PUTFIELD i : I // 访问类字段(类变量),复制给i这个变量
解决原子性问题
通过上面了解,原子性问题导致的原因是线程切换,那么我们在操作共享变量时禁止线程切换不就可以解决问题了吗?
那么就产生了“同一个时刻只有一个线程执行”,其他线程等待,这种现象我们称为“互斥”,那么互斥锁的概率就可以这理解。
Java语言提供关键字:synchronized
syn是在Java其中的一种锁的实现,可以用来修饰代码块、实例方法、静态方法
class demo{
public synchronized void method1(){
//独行区
}
public synchronized static method2(){
//独行区
}
public void method3(){
synchronized(this){//this = 实例后的 demo对象
//独行区
}
}
}
class example{
public synchronized static method2(){
//独行区
}
public void method3(){
synchronized(example.class){
//独行区
}
}
//当修饰静态方法或example.class的时候,锁定的当前类的class对象,
}
更新原子性问题的代码
static int i =0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 0; j < 1000; j++) {
synchronized (AtomicExample.class){
i++;
}
}
});
Thread t2 = new Thread(()->{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 0; j < 1000; j++) {
synchronized (AtomicExample.class){
i++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
2000
synchronized原理(可以不看)
jdk1.6开始对synchronized关键字进行的许多优化,为了减少重量级锁带来的性能开销,尽可能减轻锁的级别下解决现存并发问题。锁一共有4个状态:无锁、偏向锁、轻量级锁(自旋锁)、重量级锁,从低到高逐步升级。加锁的本质就是在锁对象的对象头写入当前线程ID,查询当前线程用的什么锁状态可以通过打印对象的对象头查看,建议ClassLayout jar包,主要看对象后的第一个字节最后三位,【001】表示无锁,【000】轻量级锁,【101】偏向锁、【010】重量级锁
这块没有具体代码展示,大部分都为理论
偏向锁
默认情况下偏向锁是开启状态,如果有线程去抢占锁,会优先抢占偏向锁,如果没有线程来竞争,会偏向该线程的ID
轻量级锁
存在线程竞争的情况下,撤销偏向锁升级到轻量级锁,操作对象头用CAS操作markword设置为指向自己线程的LR指针,设置成功后表示抢占到锁,其他没有抢占到锁的线程不会直接等待,会有一定的自旋次数(可通过JVM配置自旋次数),自旋次数会跟进竞争状态调整控制自旋的时间,自旋就是一直是尝试获取锁的状态,线程是没有阻塞的
重量级锁
自旋一定次数后没有获取到锁,进行锁升级到重量级锁,想操作系统申请资源,然后线程被阻塞挂入到等待队列,等获取到锁的线程释放锁,在通过CPU唤醒其中一个,切换上下文资源,重新获取锁。