线程安全
1.概念
线程安全是多线程编程时计算机程序代码中的一概念。在拥有共享数据的多条线程并执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的运行,不会出现数据污染等意外情况。
2.线程安全问题的原因
线程安全问题大多是由全局变量
和静态变量
引起的,局部变量逃逸也可能导致线程安全问题。
3.为什么会出现线程安全问题
多线程操作共享资源时,导致共享资源出现错乱。线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
关于线程安全问题的代码示例:(会出现并发现象)
public class Test1 {
public static void main(String[] args) {
MyThread05 mt = new MyThread05();
Thread m1 = new Thread(mt);
Thread m2 = new Thread(mt);
m1.start();
m2.start();
}
}
class MyThread05 extends Thread {
private int id;
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
id++;
}
System.out.println(this.currentThread().getName() + "--结果是:"+ id);
}
}
4.怎样解决线程安全问题
线程同步:当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,这种就称为加锁。
Java加锁的方式 :synchronized
关键字和Lock
锁的使用
5.线程安全问题解决的方法
1)同步代码块
2)同步方法
3)锁机制
方法一:同步代码块
格式:
synchronized(锁对象){
可能会出现线程安全问题的代码(访问共享数据的代码)
}
注意:
1.通过代码块的锁对象,可以是任意的对象
2.必须保证多个线程使用的锁对象是同一个
3.锁对象的作用是把同步代码块锁住,只允许一个线程在同步代码块执行
Runnable实现类
package com.example.interfacetest;
/**
* @author zwp
* @description: 多线程安全同步代码块 Runnable实现类
* @date: 2021/5/10 11:00
*/
public class RunnableImpl implements Runnable {
private int ticket = 100;
//创建一个锁对象,锁对象应创建在run方法外,因为不同的线程应该使用同一个锁对象,如果在run方法里面,就会每个线程自己创建一个锁对象,也就不是唯一了
Object object = new Object();
@Override
public void run() {
while (true) {
synchronized (object) {//访问了共享数据的代码,也就是可能出现线程安全问题的代码,写在同步代码块里面
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "is selling ticket number-->" + ticket);
ticket--;
}
}
}
}
}
再次开启多个线程,发现就没有出现线程安全问题了
/**
* @author zwp
* @description:
* @date: 2021/5/10 11:10
*/
public class RunnableDemo {
public static void main(String[] args) {
RunnableImpl runnable = new RunnableImpl();
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable);
t1.start();
t2.start();
t3.start();
}
}
运行结果:由于一张截图未展示完,以多张截图形式展示运行结果
image.png
image.png
image.png
image.png
image.png
同步代码块的原理:
使用了一个锁对象,叫同步锁,对象锁,也叫同步监视器,当开启多个线程的时候,多个线程就开始抢夺CPU的执行权,,比如现在t0线程首先得到执行,就会开始执行run方法,遇到同步代码块,首先检查是否有锁对象,发现有则获取该锁对象,执行同步代码块中的代码。之后当CPU切换线程时,比如t1的得执行,也开始执行run方法,但是遇到同步代码块检查是否有锁对象时发现没有锁对象,t1便被阻塞,等待t0执行完毕同步代码块,释放锁对象,t1才可以获取从而进入同步代码块执行。
同步中的线程,没有执行完毕是不会释放锁的,这样便实现了线程对临界区的互斥访问,保证了共享数据安全。
缺点:频繁的获取锁释放对象,降低程序效率。
方法二:同步方法
使用步骤
1.把访问了共享数据的代码抽取出来,放到一个方法中。
2.在该方法上加synchronized修饰符
格式:
修饰符 synchronized 返回值类型 方法名称(参数列表) {
method body
}
public class RunableImpl implements Runnable {
private int ticket = 100;
// 创建一个锁对象,锁对象应该创建在run方法外,因为不同的线程应该使用同一个锁对象,如果在run方法里面,就会每个线程自己创建一个锁对象,也就是不唯一了
Object object = new Object();
@Override
public void run() {
while(true) {
sellTicket();
}
}
public synchronized void sellTicket() {// 同步方法,访问了共享数据的代码,可能出现线程安全问题的代码,放到一个方法中
if(ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " is selling ticket number--> " + ticket);
ticket--;
}
}
}
同步方法也是一样锁住同步的代码,但是锁对象的是Runnable实现类对象,也就是this对象,谁调用方法,就是谁,在这里就是创建的run对象。
静态的同步方法,添加一个系那个太static修饰符,此时锁对象就不是this了,静态同步方法的锁对象是本类的class属性,class文件对象(反射)
public static synchronized void sellTicket() {
if(ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " is selling ticket number--> " + ticket);
ticket--;// 访问了变量应该定义为static的,因为静态方法要访问静态变量
}
}
}
方法三:Lock接口
java.util.concurrent.locks.Lock
Lock接口中的方法:
void lock():获取锁
void unlock():释放锁
Lock接口的一个实现类
java.util.concurrent.locks.ReentrantLock implements Lock接口
使用方法:
1、在Runable实现类的成员变量创建一个ReentrantLock对象
2、在可能产生线程安全问题的代码前该对象调用lock方法获取锁
3、在可能产生线程安全问题的代码后该对象调用unlock方法获取锁
public class RunableImpl implements Runnable {
private static int ticket = 100;
// 创建一个锁对象,锁对象应该创建在run方法外,因为不同的线程应该使用同一个锁对象,如果在run方法里面,就会每个线程自己创建一个锁对象,也就是不唯一了
Object object = new Object();
ReentrantLock l = new ReentrantLock();// 1、在Runable实现类的成员变量创建一个ReentrantLock对象
@Override
public void run() {
while(true) {
l.lock();// 2、在可能产生线程安全问题的代码前该对象调用lock方法获取锁
if(ticket > 0) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + " is selling ticket number--> " + ticket);
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// unlock方法放在finally里面,无论程序是否有出现异常,该方法都会执行,也就是都会释放锁
l.unlock();// 3、在可能产生线程安全问题的代码后该对象调用unlock方法获取锁
}
}
}
}
}
全局变量 局部变量 静态变量
按存储区域分:全局变量、静态全局变量和静态局部变量都存放在内存的全局数据区,局部变量存放在内存的栈区
按作用域分:
1、全局变量在整个工程文件内都有效;
2、静态全局变量只在定义它的文件内有效;
3、静态局部变量只在定义它的函数内有效,且程序仅分配一次内存,函数返回后,该变量不会消失;局部变量在定义它的函数内有效,但是函数返回后失效。
4、全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。局部变量的值不可知。
5、静态局部变量与全局变量共享全局数据区,但静态局部变量只在定义它的函数中可见。静态局部变量与局部变量在存储位置上不同,使得其存在的时限也不同,导致对这两者操作 的运行结果也不同。