海淀网站建设wzjs51,孝感公司做网站,怎么自己建设个网站,wordpress字体设置1. 什么是线程安全
线程安全指的是当多个线程同时访问一个共享的资源时#xff0c;不会出现不确定的结果。这意味着无论并发线程的调度顺序如何#xff0c;程序都能够按照设计的预期来运行#xff0c;而不会产生竞态条件#xff08;race condition#xff09;或其他并发问…1. 什么是线程安全
线程安全指的是当多个线程同时访问一个共享的资源时不会出现不确定的结果。这意味着无论并发线程的调度顺序如何程序都能够按照设计的预期来运行而不会产生竞态条件race condition或其他并发问题。
请看如下代码
我们用两个线程分别让count 5w次最后我们打印count理论得到的结果是10w
public class ThreadDemo13 {public static int count 0;public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {for(int i 0; i 50000; i) {count;}});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);}
} 运行代码: 我们发现结果与我们预期的并不相同。
其实 count 是由三个cpu指令来完成的
load 从内存中读取数据到cpu寄存器add 把寄存器中的值 1save 把寄存器中的值写回到内存中 由于线程之间是并发执行的每个线程执行到任何一条指令后都可能从cpu上调度走而且去执行其他线程于是当 t1 线程和 t2 线程并发执行时会存在以下情况。
可以看到 这种情况两次 count 实际上只让count1了并且实际情况中 t1 的load和add之间又能有更多的指令那样则会导致 多个count 只会令count1 所以导致了结果与预期不符合
这类问题就称为线程不安全问题。 2. 线程安全问题及解决 从上面的示例我们可以发现导致线程不安全的原因是count 这三个指令不是整体执行的于是我们要解决这个问题就可以想办法使这三个指令为一个整体
2.1 锁
在Java中我们可以通过加锁的方式来保证线程安全锁具有 “互斥” “排他” 的特性
在Java中加锁的方式有很多种最主要的方式是通过 synchronized 关键字
语法
synchronized(锁对象) {//要加锁的代码
}
加锁的时候需要“锁对象”如果一个线程用一个锁对象加上锁以后其他线程也尝试用这个锁对象来加锁就会产生阻塞BLOCKED直到前一个对象释放锁
锁对象是一个Object对象
我们给上述ThreadDemo13中 t1, t2 中的count加上锁
public class ThreadDemo13 {public static int count 0;public static void main(String[] args) throws InterruptedException {//随便创建一个对象作为锁对象,因为所有类默认继承于Object类//所以任意一个类的对象都可以作为锁对象Object locker new Object();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;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count count);}
}此时我们再次运行代码 发现结果正确 这是因为我们给 t1 t2 中的count都加上了同一个锁运行代码时 t1 中的count 没有执行完时t2中的 count 拿不到锁就不会执行同理t2 中的count 没有执行完时t1中的 count 也拿不到锁也就不会执行。所以就保证了线程安全。
这里我们可以这样理解加锁操作给代码加锁就是规定这段代码必须拿到对应的锁才能执行如果另一个线程中也有代码加了这把锁即相同的锁对象同样这段代码也必须拿到这个锁才能执行但是这个锁只有一个所以同一时间只能执行一段代码另一段代码只能等上一段代码执行完把锁释放了才能拿到锁进而执行
注意
加了锁的代码任然可能中途被调度出cpu只不过调度出cpu后任然是加锁状态只有以同一个锁对象加锁的线程间会产生锁竞争 synchronized 还有几种写法
1. 下面这个代码是否是线程安全的
class Test {public static int count 0;public void add() {synchronized (this) {count;}}
}
public class ThreadDemo14 {public static void main(String[] args) throws InterruptedException {Test t new Test();Thread t1 new Thread(() - {for(int i 0; i 50000; i) {t.add();}});Thread t2 new Thread(() - {for(int i 0; i 50000; i) {t.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count t.count);}
}
注意this指的是当前对象我们发现 调用add的都是t所以 t1, t2 中 都是通过 t 来加锁所以存在锁竞争这段代码是线程安全的 2. 锁可以加在方法上面
class Test1 {public static int count 0;synchronized public void add() {count;}
}
public class ThreadDemo15 {public static void main(String[] args) throws InterruptedException {Test1 t new Test1();Thread t1 new Thread(() - {for(int i 0; i 50000; i) {t.add();}});Thread t2 new Thread(() - {for(int i 0; i 50000; i) {t.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count t.count);}
}这种写法与上面效果相同都是通过当前对象加锁 2.2 synchronized 特性 下列代码能否正常打印Ting
public class ThreadDemo16 {public static void main(String[] args) {Object locker new Object();Thread t new Thread(() - {synchronized (locker) { //1synchronized (locker) { //2System.out.println(Ting);}// 3}// 4});t.start();}
}
答案是可以的 解释在1位置第一次使用locker加锁很明显这里是可以加上的在2位置这里也在尝试使用locker进行加锁按照我们上面的理解这里的锁是加不上的但是最后却输出了Ting这里是因为这两次加锁是同一个线程在进行这种操作是允许的这个特性称为可重入这是Java开发者为了防止出现死锁而设计的
注意这种写法在1位置才会加锁在2 位置时不会真的加锁在3位置也不会释放锁在4位置才会释放锁 对于可重入锁内部会有一个加锁次数的计数器当加锁时计数器为0 才会加锁每“加一次锁”计数器1而每出一个“}”计数器-1为0时才释放锁
2.3 死锁的三种典型场景
1. 一个线程一把锁
如同上面所讲的如果锁是不可重入锁并且一个线程用这把锁加锁两次就会出现死锁
2. 两个线程 两把锁
线程 1 获取到锁 A 线程 2 获取到锁 B 在这种情况下 1尝试获取 B, 2尝试获取 A
示例代码
public class ThreadDemo17 {public static void main(String[] args) {Object A new Object();Object B new Object();Thread t1 new Thread(() - {synchronized (A) {try {//等 t2 拿到BThread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (B) {System.out.println(t1拿到了两把锁);}}});Thread t2 new Thread(() - {synchronized (B) {try {//等 t1 拿到AThread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (A) {System.out.println(t2拿到了两把锁);}}});t1.start();t2.start();}
}
3. N个线程M把锁
类似于上面的两个线程两把锁的问题
下面简单画个图演示这种情况 这种情况下每个线程都在等待左边的锁被释放形成一个死锁
解决对每把锁都进行编号规定每个线程都必须先获取编号小的锁再获取编号大的锁于是线程1在获取到锁A前是不会获取锁F的所以就避免了上述情况。 2.4 内存可见性引起的线程安全问题
在Java中多线程共享内存可能会导致内存可见性问题从而引起线程安全问题。简单来说内存可见性是指当一个线程修改了共享变量的值时这个新值可能不会立即被其他线程所看到
示例代码
public class ThreadDemo18 {public static int flag 0;public static void main(String[] args) {int a 0;Thread t1 new Thread(() - {while(flag 0) {}System.out.println(t1线程结束);});Thread t2 new Thread(() - {try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(把flag置为1);flag 1;});t1.start();t2.start();}
}
当我们运行代码发现 flag被置为1后 t1线程并没有结束 这里我们主要查看这段代码 这段代码的核心指令只有两条 1. 读取内存中flag的值到cpu寄存器里 2. 拿寄存器里的值和 0 比较
在上述循环中的循环速度是非常快的 一秒钟可能就运行了几亿次在这个执行过程中1操作每次读取的结果都是一样的并且我们知道内存的读写速度相对于2操作的比较速度是慢得多的在这个循环中九成九的时间都在执行1操作并且运行了很多次(几亿甚至上百亿读取的值都没有变化此时JVM 就可能做出优化不再执行 1 操作直接用之前寄存器中的值和0做比较。当后面 flag的值变为1后t1 线程中寄存器中存的值还是0所以循环不会结束。
我们可以在while中加一个sleep让循环变慢
public class ThreadDemo18 {public static int flag 0;public static void main(String[] args) {int a 0;Thread t1 new Thread(() - {while(flag 0) {try {Thread.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(t1线程结束);});Thread t2 new Thread(() - {try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(把flag置为1);flag 1;});t1.start();t2.start();}
}
运行代码
我们发现循环可以正常结束。
这是因为不加sleep时一秒钟循环几亿次操作 1 的整体开销占比是非常大的优化的迫切程度就更高加了sleep后一秒循环1000次操作 1 的整体开销占比就小很多了优化的迫切程度也就每那么高。
Java中提供了 volatile 关键字可以使上述优化被关闭
public class ThreadDemo18 {volatile public static int flag 0;public static void main(String[] args) {int a 0;Thread t1 new Thread(() - {while(flag 0) {}System.out.println(t1线程结束);});Thread t2 new Thread(() - {try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(把flag置为1);flag 1;});t1.start();t2.start();}
}
运行结果 volatile 有两个功能
1. 保证内存可见性 2. 防止指令重排序
2.5 线程饿死 Java中的线程饥饿Thread Starvation是指某个或某些线程无法获取到所需的CPU时间或其它系统资源从而陷入长时间的等待状态无法继续正常执行。这种情况可能会导致程序性能下降、响应时间延长甚至出现死锁等严重问题。
线程饥饿通常是由于以下几个原因引起的 CPU资源被占用当有一个或多个线程占用了大量的CPU资源时其他线程可能无法获得足够的CPU时间从而无法正常执行。 长时间等待资源当某个线程需要等待某个资源如锁、I/O操作等时如果该资源一直被其他线程占用那么该线程可能会长时间等待从而导致线程饥饿。 示例代码 public class ThreadDemo19 {public static void main(String[] args) throws InterruptedException {Object locker new Object();Thread t1 new Thread(() - {synchronized (locker) {while(true) {//模拟长时间占用锁}}});Thread t2 new Thread(() - {synchronized (locker) {System.out.println(执行了t2);}});t1.start();//确保t1先拿到lockerThread.sleep(100);t2.start();}
} 线程优先级不足当有多个线程同时竞争某个资源时如果优先级较低的线程一直无法获得该资源那么它们可能会陷入长时间的等待状态。 例如现有 123 三个线程线程1 的优先级更高现在线程1 拿到了锁但是现在某个条件不满足线程1无法执行所以线程1 由把锁释放了但是线程 1 释放锁之后 任然会参与到锁竞争中又由于 线程1的优先级高于线程2和线程3所以任然是线程1拿到锁于是导致了死锁。这种情况下我们可以使用 wait/notify 解决 让线程1 在条件满足时 再尝试获取锁
2.6 wait / notify wait()方法 wait()方法是Object类中定义的方法可以在任何对象上被调用。它使当前线程释放对象的锁并让线程进入等待状态直到其他线程调用相同对象上的notify()或notifyAll()方法将其唤醒。调用wait()方法会导致当前线程进入等待队列并释放对象的监视器锁即释放synchronized块或方法中的锁允许其他线程获得该锁并执行相应操作。wait()方法可以指定等待的超时时间如果在指定的时间内没有被唤醒则线程会自动苏醒。 notify()方法 notify()方法也是Object类中定义的方法用于唤醒等待在相同对象上的某个线程。它会选择性地通知等待队列中的一个线程表示该线程可以尝试重新获得对象的锁。如果有多个线程在等待相同对象上的锁那么只有其中一个线程会被唤醒具体唤醒哪个线程是不确定的。notifyAll()方法则会唤醒等待队列中的所有线程。 示例
public class ThreadDemo20 {public static void main(String[] args) throws InterruptedException {Object locker new Object();Thread t1 new Thread(() - {synchronized (locker) {try {System.out.println(wait之前);//wait必须在synchronized内部因为要释放锁的前提是得加上锁locker.wait();System.out.println(wait之后);} catch (InterruptedException e) {//wait 和 sleep join都是一类的可能会被提前唤醒需要捕获异常e.printStackTrace();}}});Thread t2 new Thread(() - {synchronized (locker) {System.out.println(notify之前);//Java特别规定notify也必须在synchronized内部locker.notify();System.out.println(notify之后);}});t1.start();Thread.sleep(1000);t2.start();}
} 执行过程
t1 执行后会立刻拿到锁并且打印 “wait之前” 然后进入wait方法 释放锁阻塞等待然后等待1秒t2开始执行拿到锁 打印 “notify”之前 然后执行 notify让 t1 停止阻塞重新参与锁竞争
注意wait()可以设置最大等待时间具体规则和join相同