一、线程安全的发生
我们可以这样认为,
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境下出现的结果,则说这个程序是线程安全的。
- 看一段示例代码:
public class Main {
static int num=0;
static int count=1000000;
static class A extends Thread{
@Override
public void run() {
for (int i = 0; i < count; i++) {
num++;
}
}
}
static class B extends Thread{
@Override
public void run() {
for (int i = 0; i <count ; i++) {
num--;
}
}
}
public static void main(String[] args) throws InterruptedException {
A a =new A();
a.start();
B b =new B();
b.start();
System.out.println(num);
}
}
运行结果:
你会发现,上述代码在正常情况下应该输出是0,但某些时候会出现和理想结果不一致的情况,此时,就表示线程出现了安全问题。
二、线程安全发生的原因
-
线程的抢占式执行
线程是一个抢占式的执行过程,具有随机性,由操作系统内核实现。
-
多线程修改共享数据,原子性被破坏
内存中的共享数据被多个线程所共享,单个线程的修改操作不是原子的,多个线程在执行过程中相互 穿插,就破坏了上个线程修改的数据。也就无法保证线程安全。
-
内存可见性
内存可见性指一个线程对共享变量值的修改,能够及时被其他线程看到。比如:
多个线程同时操作一个内存,一个读内存,一个写内存;写操作的线程进行修改的时候,读线程可能读取到的都是修改之前的值,也可能读取到的是修改之后的值,仍然存在不确定性,这也会带来线程不安全。 -
指令重排序
在JDK中,JAVA语言为了维持顺序内部的顺序化语义,也就是为了保证程序的最终运行结果需要和在单线程严格意义的顺序化环境下执行的结果一致,程序指令的执行顺序有可能和代码的顺序不一致,这个过程就称之为指令的重排序。
三、如何考虑线程安全问题
- 情况1:尽可能让程序间不做数据共享,各个线程独立工作,就不需要考虑线程安全问题;
- 情况2:如果非要有共享操作,就尽可能不做数据修改,而是只读操作;
- 情况3:一定会出现线程安全问题时,从系统角度分析原因:
(1)原子性被破坏;
(2)由于内存可见性问题,导致某些线程读取到了“脏”数据;
(3)由于代码重排序问题,导致线程间数据配合出现问题。
四、线程安全保护机制
- synchronized 同步代码快
(1)不使用synchronized ,代码演示:
public class UseSyn {
static class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 1_0000 ; i++) {
System.out.println("线程1正在执行......");
}
}
}
static class MyThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 1_0000; i++) {
System.out.println("线程2正在执行......");
}
}
}
public static void main(String[] args) {
Thread thread1 =new MyThread1();
thread1.start();
Thread thread2 =new MyThread2();
thread2.start();
}
}
执行结果:
(2)使用synchronized ,代码演示:
//加锁操作
public class UseSyn {
//定义锁对象
private static Object lock =new Object();
static class MyThread1 extends Thread{
@Override
public void run() {
synchronized (lock){
for (int i = 0; i < 1_0000 ; i++) {
System.out.println("线程1正在执行......");
}
}
}
}
static class MyThread2 extends Thread{
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 1_0000; i++) {
System.out.println("线程2正在执行......");
}
}
}
}
public static void main(String[] args) {
Thread thread1 =new MyThread1();
thread1.start();
Thread thread2 =new MyThread2();
thread2.start();
}
}
执行结果:
综上结果分析,对线程加锁后,程序不会出现交替执行的结果,synchronized 会对临界区资源进行加锁,加锁后的资源线程间互斥的访问。
- Lock锁
(1)基本用法,相对synchronized使用更加灵活
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 可重入锁
*/
public class UseLock {
static int r=0;
static final int count=10000000;
//定义两个线程,分别对r进行加和减的操作
static class Add extends Thread{
private Lock o;
public Add(Lock o) { this.o=o; }
@Override
public void run() {
//加锁
o.lock();
try {
//临界区资源
for (int i = 0; i < count; i++) {
r++;
}
}finally {
o.unlock();
}
}
}
//减操作
static class Sub extends Thread{
private Lock o;
public Sub(Lock o) { this.o =o;
}
@Override
public void run() {
for (int i = 0; i < count; i++) {
o.lock();
try {
//锁粒度较上要小,粗略估计,锁的粒度越小,并发性越好
r--;
}finally {
o.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
//Lock实现类,可重入锁
Lock lock =new ReentrantLock();
Add add =new Add(lock);
add.start();
Sub sub =new Sub(lock);
sub.start();
add.join();
sub.join();
//上述线程,add对变量 r进行了count次加操作,sub对r进行了count次减的操作,
//理论上,r的输出结果应该是 0
System.out.println(r);
}
}
(2)尝试请求锁,未获取到返回false
/**
* 尝试请求锁
*/
public class UseTryLock {
//定义一个锁
private static final Lock lock =new ReentrantLock();
static class Mythread extends Thread{
@Override
public void run() {
//尝试加锁
boolean b = lock.tryLock();
if (b==true){
System.out.println("加锁成功");
System.out.println("子线程进入临界区");
}else {
System.out.println("加锁失败");
}
}
}
public static void main(String[] args) throws InterruptedException {
lock.lock();
Mythread t =new Mythread();
t.start();
//主线程休眠2s
TimeUnit.SECONDS.sleep(2);
t.join();
}
}
(3)可中断锁,线程中断时,可中断当前锁
/**
* 可中断锁
*/
public class UseTryLockInterrupt {
//定义一个锁
private static final Lock lock =new ReentrantLock();
static class Mythread extends Thread{
@Override
public void run() {
//尝试加锁
try {
//可中断锁
lock.lockInterruptibly();
System.out.println("加锁成功");
System.out.println("子线程进入临界区");
} catch (InterruptedException e) {
System.out.println("收到停止信号,停止运行");
}
}
}
public static void main(String[] args) throws InterruptedException {
lock.lock();
Mythread t =new Mythread();
t.start();
//主线程休眠2s
TimeUnit.SECONDS.sleep(2);
t.interrupt();
t.join();
}
}
执行结果:
总结,Lock锁相对synchronized使用更加灵活,它使用锁的策略包括可以一直请求锁,也可以尝试请求锁,可以使用带超时的尝试请求锁,还可以带中断的锁,使用lock更加灵活。