首页 / JAVA  

java 线程安全的解决方案

多线程的数据安全性问题分为三种:原子性、可见性和有序性。

原子性:

是指我们的一系列操作要么全部都做,要么全部不做。

可见性
java内存模型规定,每个java线程可以有自己的工作内存,工作内存是线程私有的,而共享内存(主存)是线程共享的。线程工作内存中会有共享变量的副本,当线程对一个共享变量进行写入时,会先写入线程私有的工作内存,然后再刷新到主存中。
这样就可能会产生一个问题:线程1改变了共享变量的值,在还未刷新到主存时候,线程2去读取这个变量,此时线程2将看不到线程1对这个变量所做的修改。这就是多线程并发带来的数据可见性问题。


java中可以通过申明一个变量为volatile来解决可见性问题。线程读取一个volatile变量时JMM会强制要求线程从主内存中读取,写一个volatile变量时JMM会要求立马刷新到主内存中。java中通过synchronized加锁后的写入也可以保证数据的可见性。
volatile能够解决可见性和有序性但是不能保证原子性,如果需要保证原子性则需要加锁。这里有一点需要注意的是:volatile类型的long,double变量的读取是原子读取,而非volatile的long,double类型变量读取是非原子读取,所以也可以说volatile在一定程度上解决了原子性问题。


有序性:

如果在本线程内观察,所有操作都是有序的,但是如果在一个线程观察另一个线程,所有的操作都是无序的。产生这种问题的根本原因在于"指令重排序"和"工作内存和主内存同步延迟"。java中volatile变量通过内存屏障来防止指令重排序从而保证有序。


为了解决多线程的数据安全性问题,java中引入了锁,锁是为了防止在多线程同时读写一个共享内存时出现的并发数据安全性问题。Java中的锁大体分为两类:"synchronized"关键字锁和"JUC"(java.util.concurrent包)中的locks包和atomic中提供的锁。


1.创建线程的两种方式

01.继承Thread类。
02.实现Runnable接口。(这种方式较为常用)

2.实现Runnable接口的好处

01.将线程的任务从线程的子类中分离出来,进行了单独的封装。按照面向对象的思想将任务的封装成对象。
02.避免了java单继承的局限性。

产生线程安全的原因:

  • 多个线程在操作共享的数据。
  • 操作共享数据的线程代码有多条。
  • 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。就会导致线程安全问题的产生。

解决思路:
就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程时不可以参与运算的。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。在java中,用同步代码块就可以解决这个问题。

synchronized 同步代码块的格式

synchronized(对象)
{
需要被同步的代码;
}

同步的好处:解决了线程的安全问题。

同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。

同步的前提:同步中必须有多个线程并使用同一个锁。

public class MyThread implements  Runnable{
public static int count = 10; // 票数
public Object objLock = new Object() ;
@Override
public void run() {
while(count>0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
sale();
}
}

public void sale(){
/**
* 使用synchronized 给对象加锁,只有拿到这个对象锁的线程才能进入到内部进行程序操作,拿到锁的线程执行完
* 内部的程序后,会自动释放对象锁,进行一下次的锁竞争中
*/
synchronized(objLock){
if(count > 0) {
System.out.println(Thread.currentThread().getName()+"-卖出第:"+(20 - count +1));
count--;
}
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread mThread1 = new Thread(thread,"售票窗口01");
Thread mThread2 = new Thread(thread,"售票窗口02");
Thread mThread3 = new Thread(thread,"售票窗口03");
mThread1.start();
mThread2.start();
mThread3.start();
}
}
售票窗口02-卖出第:1
售票窗口03-卖出第:2
售票窗口01-卖出第:3
售票窗口02-卖出第:4
售票窗口03-卖出第:5
售票窗口01-卖出第:6
售票窗口02-卖出第:7
售票窗口03-卖出第:8
售票窗口01-卖出第:9
售票窗口02-卖出第:10

这个方案只是最基本的解决方案

注意点:虽然加synchronized关键字可以让我们的线程变的安全,但是我们在用的时候也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,

Lock
先来说说它跟synchronized有什么区别吧,Lock是在Java1.6被引入进来的,Lock的引入让锁有了可操作性,什么意思?就是我们在需要的时候去手动的获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。
public class MyThread implements  Runnable{
public static int count = 0;
private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子类
@Override
public void run() {
method();
}
private void method(){
lock.lock(); // 获取锁对象
try {
System.out.println("线程名:"+Thread.currentThread().getName() + "获得了锁");
System.out.println("count:"+count++);
Thread.sleep(2000);
}catch(Exception e){
e.printStackTrace();
} finally {
System.out.println("线程名:"+Thread.currentThread().getName() + "释放了锁");
lock.unlock(); // 释放锁对象
}
}

public static void main(String[] args) {
MyThread thread = new MyThread();
Thread mThread1 = new Thread(thread,"线程01");
Thread mThread2 = new Thread(thread,"线程02");
Thread mThread3 = new Thread(thread,"线程03");
mThread1.start();
mThread2.start();
mThread3.start();
}
}
线程名:线程01获得了锁
count:0
线程名:线程01释放了锁
线程名:线程02获得了锁
count:1
线程名:线程02释放了锁
线程名:线程03获得了锁
count:2
线程名:线程03释放了锁

进入方法我们首先要获取到锁,然后去执行我们业务代码,这里跟synchronized不同的是,Lock获取的所对象需要我们亲自去进行释放,为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的。




2019-10-22