?回归初心
文章目录
什么是线程安全问题?
当多个线程共享同一个全局变量,做写的时候(如count++),可能会受到其他线程的干扰,导致数据有问题,这种现象叫做线程安全问题。
做读操作的时候,不会导致线程安全问题。
局部变量会发生线程安全问题吗?
多个线程共享一个局部变量,做写的操作,会发生线程安全问题吗?
不会,局部变量的定义是run方法中的变量,局部变量不是全局的,每个线程里的局部变量不会影响其他线程中的局部变量。
什么是线程之间的同步?
这里的同步和之前的同步异步不是同一个概念, 线程之间的同步的意思是保证数据的原子性,即数据不能受到其他线程的干扰。
线程不安全例子
两个窗口抢票案例:
package duoxiancheng;
import lombok.SneakyThrows;
class ChuangKouThread implements Runnable {
private int trainCount = 100;
@SneakyThrows
public void run() {
while (trainCount > 0){
Thread.sleep(50);
sale();
}
}
public void sale() {
System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
public class Qiangpiao {
public static void main(String[] args) {
ChuangKouThread chuangKouThread = new ChuangKouThread();
Thread t1 = new Thread(chuangKouThread, "窗口1");
Thread t2 = new Thread(chuangKouThread, "窗口2");
t1.start();
t2.start();
}
}
线程安全问题有什么解决办法?
解决办法有哪些?
- synchronized
- lock — jdk1.5包
lock和synchronized有什么区别?
synchronized自动设置锁开关,lock手动设置
使用同步代码块解决线程安全问题
什么地方需要加锁?
真正操作共享全局变量时加锁
线程安全问题的解决的基本思想?
定义对象锁,使用synchronized代码块包裹需要解决线程安全问题的地方。synchronized代码块判断谁先拿到对象锁,如果已有线程在使用了,还未被释放时 ,线程将进行等待,直到对象锁被释放,再执行代码块的内容。
private int trainCount = 100;
@SneakyThrows
public void run() {
while (trainCount > 0){
Thread.sleep(50);
sale();
}
}
private Object mutex = new Object();
public void sale() {
synchronized (mutex){
if (trainCount > 0){
System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
}
上面这段代码,如果把sale方法中判断票数是否大于0的逻辑判断代码注释掉,会出现第101张票,为什么?
线程等待争抢最后一张票时,若不加逻辑判断就会产生第101张票。
使用同步代码块有哪些条件?
- 至少有两个线程以上,需要发生同步
- 多个线程想要同步,必须要用同一把锁
- 同步代码块中应保证只有一个线程执行
线程同步的原理?
-
有一个线程已经拿到锁,其他线程获得cpu执行权时也不会执行同步代码块,等待线程释放锁。
-
代码执行完毕或者是程序抛出异常时,锁会被释放掉
-
如果当前线程获取锁,则执行同步代码块。
线程同步有什么缺点?
- 效率非常低,多个线程需要判断锁,消耗大量资源。
- 线程间会争抢锁的资源。
- 如果有线程不释放锁,则会产生死锁问题
使用同步函数解决线程安全问题
什么是同步函数?
在方法上修饰synchronized的函数称为同步函数
例子:
public synchronized void sale() {
if (trainCount > 0){
System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
同步函数使用的是什么锁?如何证明?
this锁。
两个线程之间实现同步,一个线程使用this锁同步代码块,另一个线程使用同步函数,如果这两个线程同步,说明同步函数使用的是this锁。把this锁换成Object锁,则出现重复数据
测试代码:
class ChuangKouThread implements Runnable {
private int trainCount = 100;
private boolean flag;
@SneakyThrows
public void run() {
String name = Thread.currentThread().getName();
flag = name.equals("窗口1");
if (flag) {
while (trainCount > 0) {
Thread.sleep(50);
sale();
}
} else {
while (trainCount > 0) {
Thread.sleep(50);
synchronized (this) {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
}
}
}
public synchronized void sale() {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
}
public class Qiangpiao {
public static void main(String[] args) {
ChuangKouThread chuangKouThread = new ChuangKouThread();
Thread t1 = new Thread(chuangKouThread, "窗口1");
Thread t2 = new Thread(chuangKouThread, "窗口2");
t1.start();
t2.start();
}
}
所以,一个线程使用同步函数,另一个线程使用this锁,可以线程同步。
所以,一个线程使用同步函数,另一个线程使用同步代码块,如果是非this锁,不能线程同步。
为什么同步函数设计用this锁?
因为底层设计觉得这样更合理一些(雾)
使用静态同步函数解决线程安全问题
什么是静态同步函数?
在方法上加synchronized称为同步函数,同步函数又分为两种,非静态和静态。
同步函数使用的是this锁。
非静态同步函数,有static关键字进行修饰的synchronized。
静态同步函数使用的是什么锁?
不使用this锁,静态方法里面不可能有this,使用的是当前的字节码文件。
当一个变量被static修饰时,这个变量存放永久区,当class文件被加载时就会被初始化。
java中字节码文件用类名.class定义
证明静态同步函数使用的是字节码文件:
class ChuangKouThread implements Runnable {
private static int trainCount = 100;
private boolean flag;
Object mutex = new Object();
@SneakyThrows
public void run() {
String name = Thread.currentThread().getName();
flag = name.equals("窗口1");
if (flag) {
while (trainCount > 0) {
Thread.sleep(50);
sale();
}
} else {
while (trainCount > 0) {
Thread.sleep(50);
synchronized (ChuangKouThread.class) {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
}
}
}
public static synchronized void sale() {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
}
注意事项
线程同步的共享全局变量是静态还是非静态,和是同步函数还是非同步函数没有关系。
小试牛刀题
两个线程,一个线程使用同步函数,另一个线程使用静态同步函数能实现同步吗?
不能,因为同步函数使用this锁,静态同步函数使用当前字节码文件。
加锁和同步的概念
加锁保证同步,同步是保证数据安全和原子问题。
目前内容和分布式锁、高并发和jvm锁没有任何关系
多线程死锁
什么是多线程死锁?
同步中嵌套同步,导致锁无法释放,程序卡死。
演示例子
class ChuangKouThread implements Runnable {
private int trainCount = 100;
private boolean flag;
Object mutex = new Object();
@SneakyThrows
public void run() {
String name = Thread.currentThread().getName();
flag = name.equals("窗口1");
if (flag) {
while (true) {
synchronized (mutex) {
sale();
}
}
} else {
while (true) {
sale();
}
}
}
@SneakyThrows
public synchronized void sale() {
synchronized (mutex) {
if (trainCount > 0) {
Thread.sleep(50);
System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
}
}
死锁过程分析
线程1先拿到同步代码块mutex锁,要拿到同步函数的this锁时,被线程2先拿到同步函数的this锁,等待
线程2先拿到同步函数的this锁,要拿到同步代码块mutex锁时,被线程1先拿到同步代码块的mutex锁,等待
这个过程即同步中嵌套同步,互相不释放
多线程的三大特性
三大特性有哪些?
- 原子性
保证线程独一无二,目的是保证线程安全 - 可见性
java内存模型(见下) - 有序性
使用join wait notify方法控制线程的有序进行,主要运用在多线程之间通讯
JAVA内存模型
什么是java内存模型?
java内存模型和java内存结构不是一回事,java内存模型属于多线程可见性jmm,java内存结构属于jvm内存分配。要注意区分!
java内存模型决定了一个线程与另一个线程是否可见。
java内存模型中主内存主要存放共享的全局变量,而私有本地内存主要存放本地线程私有变量。
共享全局变量如Count存放于主内存,多个线程可以对其操作,在线程内部又存在有本地私有内存。
线程要对共享数据进行写操作时,线程要先从主内存中拷贝一份放在自己的本地内存中,再进行写操作,线程不安全问题产生于此。
具体地,t1和t2同时对count进行写的操作,不会在主内存进行写操作,而是在本地内存中对副本做写操作,而数据副本被t1和t2同时取出,其值都为0, 假定线程都进行写操作count++,更新完副本数据后再将数据刷新到主内存里面去,主内存count值更改了两次,其值都为1。
但如果t1和t2并不是同时进行写操作,主内存能进行通知,保证线程的安全,具体地,t1将count值刷新到主内存后,主内存会立刻通知t2的本地内存,这时,t2的本地内存中count的副本改为新的值,这样就保证了一定的线程的安全。
所以主内存通知线程来不来得及,就是否产生线程安全问题。如果主内存通知其他线程不及时,就会产生数据的冲突。
以上就是java内存模型。
可见性-Volatile关键字
Volatile关键字的作用?
让变量在多个线程之间可见,即本地内存的值发生改变后立马通知给另一个线程。
Volatile关键字的缺点?
不保证原子性,即不保证线程安全性问题。
代码样例
class ThreadValatile extends Thread {
public boolean flag = true;
//public volatile boolean flag = true;
public void run() {
System.out.println("子线程开始执行");
while (flag) {
}
System.out.println("子线程结束执行");
}
public void setFlag(boolean bool) {
flag = bool;
}
}
public class Vali {
@SneakyThrows
public static void main(String[] args) {
ThreadValatile thread = new ThreadValatile();
thread.start();
Thread.sleep(3000);
thread.setFlag(false);
System.out.println("Flag值修改为false");
Thread.sleep(1000);
System.out.println(thread.flag);
}
}
在线程运行时,主内存立的Flag为true,线程将flag拷贝了一份副本到自己的本地内存中;
主线程修改了共享的全局变量Flag,值修改为false,即主内存的值改成false了,但是代码仍然会在死循环,说明本地内存的值仍然为true,这时候flag加上volatile关键字,一但主内存flag值被修改,则立马通知其他的子线程并子线程的本地内存与主内存同步。
原子类-AtomicInteger
非原子性样例
class ThreadValatile extends Thread {
public int count = 0;
public void run() {
for (int i = 0; i < 1000; i++) {
count++;
}
System.out.println(Thread.currentThread().getName() + ": " + count);
}
}
public class Vali {
@SneakyThrows
public static void main(String[] args) {
ThreadValatile[] threadList = new ThreadValatile[10];
for (int i = 0; i < threadList.length; i++) {
threadList[i]=new ThreadValatile();
threadList[i].start();
}
}
}
这段代码结果:
都是1000,相当每个线程只都对自己的局部变量进行修改,并没有共享数据。想要共享count数据,则在count前加上volatile static关键字,static数据只存放一次在静态区中,所有线程都会共享,并即时通知其他线程。
将count添加volatile static关键字后,再多次执行,发现会产生不一样的结果,并且count不等于10000:
所以这也说明了volatile关键字并不能保证数据的原子性。
AtomicInteger样例
class ThreadValatile extends Thread {
public static AtomicInteger count = new AtomicInteger();
public void run() {
for (int i = 0; i < 1000; i++) {
// count++
count.incrementAndGet();
}
System.out.println(Thread.currentThread().getName() + ": " + count.get());
}
}
public class Vali {
@SneakyThrows
public static void main(String[] args) {
ThreadValatile[] threadList = new ThreadValatile[10];
for (int i = 0; i < threadList.length; i++) {
threadList[i] = new ThreadValatile();
threadList[i].start();
}
}
}
其他Atomic的API
自行查阅java的API手册
我是在这里看的https://www.matools.com/api/java8