接着介绍多线程安全问题.
由于线程是随机调度,抢占式执行的,随机性就会导致程序的执行顺序产生不同的结果,从而产生BUG.下面是一个线程不安全的例子.
package Demo4;
public class Demo1 {
private static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for(int i=0;i<50000;i++){
count++;//变量捕获,只能捕获final修饰的,或者事实final的
}
});
Thread t2=new Thread(()->{
for(int i=0;i<50000;i++){
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count="+count);
}
}
理想中,应该输出100000.但实际上输出的却达不到100000. 这就是多线程并发执行导致的问题.如果让两个线程串行执行,就不会出现上述问题.为什么并发执行会导致count的值达不到100000呢?原因如下:
count++这一个操作,在cpu上来看分为三个指令分别为load:把内存中的数据加载到寄存器中 add:把寄存器中的值+1 save:把寄存器中的值返回到内存中.但是由于两个线程是并发的执行count++操作,所以有可能,当线程t1执行到某个指令的时候,线程t2把他的cpu抢占走.举一个例子,t1执行add,save操作之后cpu被t2抢占并且执行完了load,add,save 之后线程t1才在cpu上执行save.那么看起来是进行了两次加法操作,实际上只加了一次.那么如何解决上述问题呢?常用的方法是,通过一些操作把三个指令设置成一个原子操作,即通过加锁操作解决上述问题.
锁,本质也是操作系统提供的功能,内核提供的功能并且通过api给应用程序了.java又对这样的系统api进行了封装.关于锁主要的操作有两个方面:1.加锁:t1加上锁之后,t2也尝试用同一个锁对象加锁就会引起阻塞等待 2.解锁:直到t1解锁之后,t2才有可能拿到锁(加锁成功).
下面通过代码介绍一下关于加锁的操作.
package Demo4;
public class Demo2 {
private static int count=0;
public static void main(String[] args) throws InterruptedException {
Object object=new Object();
Thread t1=new Thread(()->{
for(int i=0;i<50000;i++){
synchronized(object){
count++;
}
}
});
Thread t2=new Thread(()->{
for(int i=0;i<50000;i++) {
synchronized(object){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
首先:
Object object=new Object();
先创建出了一个类,使用这个对象作为锁.注意在java中任何一个类都可以作为加锁的对象.
接着:
synchronized(object){
count++;
}
synchronized后面带上().()里面就是写的锁对象.锁对象的用途是用来区分两个线程是否是针对同一个对象来进行加锁,如果是,就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待.如果不是,就不会出现锁竞争也就不会阻塞等待.
synchronized下面跟着{},当进入代码块就是给上述()锁对象进行了加锁操作.当出了代码块就是给上述()对象进行了解锁操作.
由于上述线程t1,t2都是针对object这一个对象进行加锁.因此就会引起阻塞等待,可以看作把要进行的count++操作进行了原子化.因此就不会产生前面介绍的问题了.最后结果就是100000.
Thread t1=new Thread(()->{
for(int i=0;i<50000;i++){
synchronized(locker){
count++;
}
});
Thread t2=new Thread(()->{
for(int i=0;i<50000;i++){
synchronized(locker){
count++;
}
});
可以看到只有count++是存在锁竞争的,会变成"串行执行",但是执行for循环以及其中的条件的时候仍然是并发执行的.在t1加锁之后执行load,add,save的过程中,t1是可以被调度走的可以把其他线程调度上来.但即使调度上来的是t2线程,由于t1的load,add,save还没有执行完,没有进行解锁操作所以t2也无法执行.
上述代码也可以这样写,但是较上面的写法不是好的选择,运行的速度会变慢.
package Demo4;
public class Demo2 {
private static int count=0;
public static void main(String[] args) throws InterruptedException {
Object object=new Object();
Thread t1=new Thread(()->{
synchronized(object){
for(int i=0;i<50000;i++){
count++;
}
}
});
Thread t2=new Thread(()-> {
synchronized (object) {
for (int i = 0; i < 50000; i++) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
下面针对这个代码还有别的写法:
package Demo4;
class Counter{
public int count=0;
synchronized public void add(){
//synchronized(this) {
count++;
//}
}
}
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
Counter counter=new Counter();
Thread t1=new Thread(()->{
for(int i=0;i<50000;i++) {
//synchronized(counter) {
counter.add();
//}
}
});
Thread t2=new Thread(()->{
for(int i=0;i<50000;i++){
//synchronized(counter) {
counter.add();
// }
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
上述代码更加简便,但本质都是一样的.
观察下面的代码是否存在线程安全问题:
package Demo4;
class Countrt{
int count=0;
public void add(){
count++;
}
}
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
Countrt counter=new Countrt();
Thread t1=new Thread(()->{
for(int i=0;i<50000;i++){
synchronized(counter){
counter.add();
}
}
});
Thread t2=new Thread(()->{
for(int i=0;i<50000;i++){
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
上面的代码,t2没有加锁,即使t1加锁了,但是没有引起锁冲突,t2执行过程中没有任何阻塞,没有互斥.仍然有可能使t1执行到一般的时候,被t2抢占CPU把t1执行的结果覆盖掉.
接下来介绍死锁,首先观察下面的代码:
package Demo4;
class counttr{
private int counter=0;
synchronized public void add(){
counter++;
}
public int getCounter(){
return counter;
}
}
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
counttr counter = new counttr();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized (counter) {
counter.add();
}
}
});
t1.start();
t1.join();
System.out.println(counter.getCounter());
}
}
当t1启动的时候,首先进入for循环第一次加锁肯定能够成功,但是在for循环里面又调用了add方法,尝试再一次加锁,但是前面说过,针对一个被锁定的对象加锁就会出现阻塞等待.直到锁被解除才能继续加锁,但是这个代码块中,锁被解除的条件是执行完add操作,同时执行玩add的条件是首先进行加锁.可以发现这种情况是矛盾的,把这种状况称为"死锁".但是实际上上述代码可以正常运行,并且输出50000.原因是 synchronized自己在内部进行了特殊的处理.即每个锁对象里会记录当前是哪个线程持有了这个锁,当针对这个对象进行加锁时,就会先进行判定,当前要加锁的线程是否的持有锁的线程,如果不是就阻塞,如果是就放行,不会阻塞了.这样的机制叫做可重入锁.
接着介绍三种死锁的场景:
1.锁是不可重入锁,并且一个线程针对一个对象连续加锁两次.这样的问题通过引入可重入锁即可解决.
2.两个线程两把锁,首先观察下面代码
package Demo4;
public class Demo1{
public static void main(String[] args) throws InterruptedException {
Object object1=new Object();
Object object2=new Object();
Thread t1=new Thread(()->{
synchronized(object1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(object2){
System.out.println("t1获取到了两把锁");
}
}
});
Thread t2=new Thread(()->{
synchronized(object2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(object1){
System.out.println("t2获取到了两把锁");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
启动上述线程之后,t1尝试针对object2加锁,就会阻塞等待t2释放object2同时,t2尝试针对object1加锁,就会阻塞等待t1释放object1. 但是在没有加第二把锁的时候谁都不会释放第一把锁,因此线程就无法结束了.
通过上面也可以看到这两个线程互相等待,僵住了.
3.N个线程M把锁.
上图所示有四个人在桌前吃面,有四个筷子.每个人在什么时候吃面都是不确定的.如果出现了一个极端的情况,某一时刻 四个人同时都拿起了左手边的筷子,这样就会导致每个人都无法吃到面条.也就类似与第二种常见中的两个线程互相等待的问题,只不过线程和锁变多了而已.
通过上述例子,可以看出死锁有四个必要条件:
1.锁具有互斥性(即一个线程拿到锁之后,其他线程要想拿到该锁就要阻塞等待)
2.锁不可抢占(一个线程拿到锁之后,除非自己释放锁,某则别的线程不能进行加锁操作.)
3.请求和保持(一个线程拿到一把锁之后,不释放这个锁的前提下.再尝试获取其他锁)
4.循环等待.(多个线程获取多个锁的过程中,出现了循环等待A等待B,B又等待B)
从上面死锁的四个必要调价入手,可以尝试解决死锁问题.
即约定好加锁的顺序,让所有线程都按照固定的顺序来获取锁,并且不要让锁嵌套获取.
针对二,可以做如下改动:
package Demo4;
public class Demo1{
public static void main(String[] args) throws InterruptedException {
Object object1=new Object();
Object object2=new Object();
Thread t1=new Thread(()->{
synchronized(object1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(object2){
System.out.println("t1获取到了两把锁");
}
}
});
Thread t2=new Thread(()->{
synchronized(object1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(object2){
System.out.println("t2获取到了两把锁");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
让t1执行完之后再让t2执行,这样就不会出现死锁问题了.
对于场景三,通过约定好加锁的顺序,就可以有效避免死锁了.