当多线程对共享变量有读写操作时,可能会产生指令交错,这样就会有线程安全问题,所以产生线程安全问题有两个前提
存在在多个线程间共享的变量
对共享变量有读写操作,如果都是读的操作就没有线程安全问题。
一、线程安全问题演示
public class Test4 {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count++;
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
在上边的代码中,线程t1,t2对共享变量count进行操作,t1在自增,t2在自减。如果没有线程安全问题,加减各1000次后结果会是0,但代码实际执行起来因为线程安全问题结果不一定是0
二、问题分析
注意在java中自增和自减不是原子操作,也就是++,--虽然是一句代码但不是由一条执行完成的。
其中++操作会分成这几条指令:
(1) 读取count的值
(2) 准备常数1
(3) 做加1操作
(4) 把结果写入变量count
同样的--操作也会有这几条指令
(1) 读取count的值
(2) 准备常数1
(3) 做减1操作
(4) 把结果写入变量count
当两个线程分别执行自增和自减时,因为cpu交错执行指令,上边的8条指令可能会发生交叉,我们先来分析下分别执行1次加和减时的情况
如果没有线程安全问题,加1再减一后得到的应该是0,按上边的分析,在cpu交错执行8条指令后得到了1,循环执行1000次加减操作后结果就不可控了,有可能t1已经执行了多次加1操作了但是没有写,这时切到t2了,t2执行减1后还没写,又切到t2,这样来回切换结果就不确定了。
这就是这个案例中的线程安全问题的分析,造成这种问题的本质是因为自增和自减操作不是原子操作需要多个指令来完成,而多个线程执行时这些指令之间会产生交错执行。
三、用synchronized解决线程安全问题
上边案例的问题是因为自增和自减操作不是原子操作,如果可以把这俩操作变成原子操作就能解决线程安全问题。
java中提供了synchronized
关键字可以来解决线程安全问题 ,基本语法如下
synchronized(锁对象){
//非原子性的操作(一个或者多个都行)
i++
}
可以简单理解成每个线程只有持有了锁对象时才能进到代码块中去执行,执行完代码块后会释放锁。
假设线程t1获取了锁正在执行代码块中的i++,即使它的4条指令还没执行完,这时发生了上下文切换,t2开始执行,t2执行到代码块时发现获取不到锁就不能执行代码块中的内容,t2会进入阻塞状态。
只有等t1把它的四条指令执行完并释放了锁后t2才会被叫醒去执行代码块中的内容,这样就保证了8条指令不会交错执行,因此能解决线程安全问题。
代码演示
public class Test4 {
public static int count = 0;
public static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
synchronized (lock){
count++;
}
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
synchronized (lock){
count--;
}
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
上边用synchronized包裹了非原子的操作,所以不会有线程安全问题。其中用到的锁对象可以是java中的任意对象,但要保证需要控制的两段代码块中synchronized用的是同一个锁对象,这样才能出现互斥执行的效果。
四、synchronized使用过程中的注意点
synchronized不仅可以使用在代码块上,也可以使用在方法上,以代码块的形式使用时需要执行锁对象,加在方法上则不需要执行锁对象。
4.1 使用在实例方法上
synchronized使用在实例方法上时锁住的是当前对象,即通过哪个对象调用方法就锁定它。
public class Test5 {
public static void main(String[] args) throws InterruptedException {
Person person = new Person();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
person.increment();
}
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
person.decrement();
}
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(Person.count);
}
}
class Person {
public static int count=0;
public synchronized void increment(){
count++;
}
public synchronized void decrement(){
count--;
}
}
上边的代码,synchronized锁住的是person这个对象
4.2 使用在静态方法上
synchronized用在静态方法上锁住的是类的class对象
class Person {
public static int count=0;
public static synchronized void increment(){
count++;
}
//这两个方法的加锁方式是等价的
public static void increment2(){
synchronized (Person.class){
count++;
}
}
}
五、synchronized的原理
synchronized是如何实现锁定同步代码块中的代码,同时只允许一个线程执行的效果呢?
其实依赖与操作系统提供的Monitor
对象和java的对象头。
Monitor对象有几个属性:owner,entrylist,waitset
java中每一个对象都会有对象头,对象头中有一段内容叫做markword
,
当线程t1执行到同步代码块时,会先根据synchronized锁住的对象即锁对象的markword来看这个锁对象是否已经关联了一个Monitor对象,如果没有就会从操作系统获取一个Monitor对象,把它的owner设置成t1,把它的地址存在锁对象的markword里,这样线程t1就持有了锁;
当线程t2执行到同步代码块时,先根据锁对象的markword找到monitor对象,发现owner已经有人了,t2就会进入entrylist等待,t2的状态变成Blocked,如果有t3也执行同步代码块最终t3也会进入entrylist等待;
当线程t1执行完同步代码块的内容后就会清空monitor对象的owner,然后唤醒entrylist中的线程来重新竞争owner,谁竞争到owner就可以执行同步代码块中的内容,剩下的线程继续blocked
六、jdk中线程安全的类
线程安全的类指的是多个线程同时调用同一个实例的某一个方法时是线程安全的,但如果一个线程组合调用类中的多个方法是不能保证线程安全的。
6.1 Hashtable
Hashtable是一个线程安全的map实现,它其中的方法都加了synchronized关键字,锁住的是当前实例对象,所以如果多个线程调用同时某一个方法时,synchronized保证了只有一个线程能执行同步代码块中的内容,不会有线程安全问题。
但如果是方法的组合调用,则不一定。
// map是Hashtable类型的
if(map.get("key1") == null){
map.put("key1", System.currentTimeMillis()+"");
}
上边这段代码被多线程调用时,get,put本身都能保证是原子操作,但组合起来就不是了。
假设t1先执行完map.get判断,结果为true,然后线程上下文
切换t2开始执行,t2也执行完map.get判断,结果为true,然后t2给map中put了key1,然后切换到t1,t1又给map中put了key1,这样就会产生key覆盖的问题。
所以说线程安全类中的多个方法组合起来不是线程安全的。
6.2 String
String是线程安全的,因为String对象的内容不能改变,多个线程对它实际上只能读,所以它是线程安全的。
6.3 StringBuffer
StringBuffer中的方法都加了Synchronized关键字,所以是线程安全的。
标签:count,synchronized,t2,t1,关键字,线程,public From: https://www.cnblogs.com/chengxuxiaoyuan/p/16860612.html