1. 同步
由于多线程并发存在数据不安全问题,为了保证数据的安全性需要一些特殊的手段来维持。数据不安全主要是针对修改来说的,如果一个数据只能读不能修改几乎不会产生什么安全问题。只有修改数据的时候容易产生一些差错导致多线程并发造成数据不安全。
-
从操作系统的角度来看多线程访问临界资源时并且对临界资源做一定的修改就会产生错误,操作系统为了保证临界资源的安全性,保证每次访问临界资源的线程有且只有一个。多线程访问时会进入等待队列依次排队等待。
-
Java多线程并发出现数据不安全问题的原因也大致相同,因为某些操作不是原子性的(可以拆开),例如 i++ 这种操作在CPU看来是比较麻烦的,首先要从内存中将数据i的值读入到当前线程的工作缓存工作区中,然后再将这个值+1。最后才是将值回写到内存中。至少是3步才能完成这样一个操作,因为并发是无法预料这个线程能否在被迫剥夺CPU时间之前完成这个操作有可能在回写值到内存中时此时还没回写完就已经被其他线程剥夺了CPU,那么回写失败之后的线程再去读这个值读到的还是原来的值并没有+1。
-
为了保证这种上述操作能够完成,必须将三步操作看做一个整体执行时不能中断。只有完成回写之后其他线程才能去继续修改或者读取,这样就能保证正确性。
2. synchronized修饰词
Java中为了保证多线程并发的数据安全性提供了Synchronized关键字来保证。保证非原子操作执行时不能中断,同时多个线程修改同一个临界资源时必须采用串行执行,并且必须保证前一个线程执行完后一个线程才有机会去访问。就像卫生间一样,一旦从里面上了锁只有里面的人用完了打开了门外面的人才有机会进去。否则就有可能出现两个人同时出现在卫生间中… 所以可以将synchronized看做卫生间的门锁。
- synchronized修饰非静态方法,这种修饰普通方法的实际上锁的是实例化的对象。
- 例如有一个对象test,派生出两个子线程A 和 B。这是采用静态代理的方式,那么sum是A和B共享的数据,为了保证安全给cnt方法上锁就可以保证安全。大概就是下面这样一个图。当A访问cnt方法时对象test上锁了,B就不能访问test所有的synchronized方法只有当A访问完毕打开锁B才能访问,但是B全程可以访问test的非synchronized方法。
class TestA implements Runnable{
public int sum = 0;
public synchronized void cnt(){
sum++;
}
@Override
public void run() {
for(int i = 0;i < 100000;i++){
cnt();
}
}
}
public class FunSyn1{
public static void main(String[] args) throws InterruptedException {
TestA test = new TestA();
Thread t1 = new Thread(test,"A"); //静态代理1
Thread t2 = new Thread(test,"B"); //静态代理2
t1.start(); t2.start();
Thread.sleep(5000); //主线程休眠5秒,让t1 和 t2先跑完
System.out.println(test.sum); //200000
}
}
- 如果 A 是test1的线程,B 是 test2 的线程,很明显test1 和 test2 的数据sum是不共享的并发的时候互相不干扰(如下图情况1)。但是如果对象中的sum是一个static的变量,那么数据就是共享的了(如下图情况2)。但是现在如果对cnt方法加锁并不有效,因为锁的是对象test1 和 test2,只能保证线程A和线程B只有一个访问test1,以及一个线程C和线程D访问test2。但是并不能保证A、B、C、D四个线程每次只有一个线程去操作sum啊。(好好理解这句话),也就是可以有一个线程从test1访问sum,一个线程从test2访问sum又出现了两个线程一起访问修改一个变量啊!!!
class TestB implements Runnable{
public static int sum = 0; //静态变量不属于某个具体的对象,是共有的。
public synchronized void cnt(){
sum++;
}
@Override
public void run() {
for(int i = 0;i < 100000;i++){
cnt();
}
}
}
public class FunSyn2{
public static void main(String[] args) throws InterruptedException {
TestB test1 = new TestB();
TestB test2 = new TestB();
Thread t1 = new Thread(test1,"A");
Thread t2 = new Thread(test2,"B");
t1.start(); t2.start();
Thread.sleep(5000); //主线程休眠5秒,让t1 和 t2先跑完
System.out.println(TestB.sum); //小于200000
}
}
小结:出现这种原因是因为加锁的对象不同,对象锁对于这种情况只能锁住从当前对象去访问的线程数目只有一个,但是并不能锁住所有的对象。也就是通向静态sum的路有多条,虽然能保证每条路上只有一辆车,但是我有多条路自然有多辆车可以通过不同的路到达sum。所以对于上面的情况2应该加锁的是sum变量或者是将通向sum的路抽象出来一条路并且这条路每次只有一个车能过,而不是对象上锁这样就能保证访问sum的每次只有一个线程
- synchronized修饰静态方法,这个锁就是当前类的class对象锁。由于静态成员不属于任何一个实例对象是类成员,因此通过class对象锁可以控制静态成员的并发操作。这样就确保了通向sum的路只有一条并且这条路上每次只有一辆车。注意如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例化对象锁
class TestC implements Runnable{
static int sum = 0;
public static synchronized void cnt(){
sum++;
}
@Override
public void run() {
for(int i = 0;i < 100000;i++){
cnt();
}
}
}
public class StaticSyn {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new TestC(),"A");
Thread t2 = new Thread(new TestC(),"B");
t1.start(); t2.start();
Thread.sleep(5000);
System.out.println(TestC.sum); //200000
}
}
- synchronized修饰代码块,当需要编写的代码量较大并且只有少部分代码需要进行同步操作如果对方法进行同步可能会导致性能偏低,为了解决这个问题可以将这少部分需要同步的代码提取成一个同步代码块。
-
synchronized代码块锁实例化对象,类似于synchronized修饰非静态方法
class TestD implements Runnable{ static int sum = 0; @Override public void run() { synchronized (this){ //对象锁 for(int i = 0;i < 100000;i++){ sum++; } } } } public class SynBlockThis { public static void main(String[] args) throws InterruptedException { TestD test = new TestD(); Thread t1 = new Thread(test,"A"); Thread t2 = new Thread(test,"B"); t1.start(); t2.start(); Thread.sleep(5000); System.out.println(test.sum); //200000 } }
-
synchronized代码块锁class类,类似于synchronized修饰静态方法
class TestE implements Runnable{ static int sum = 0; @Override public void run() { synchronized (SynBlockClass.class){ //类锁 for(int i = 0;i < 100000;i++){ sum++; } } } } public class SynBlockClass{ public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new TestE(),"A"); Thread t2 = new Thread(new TestD(),"B"); t1.start(); t2.start(); Thread.sleep(5000); System.out.println(TestE.sum); //200000 } }
3. 总结
synchronized的用法大概就这几种情况但是变化很多,主要去判断应该锁对象还是锁类,怎么锁才是关键。但是synchronized并不是万能的,会有死锁的情况发生。
-
一个线程持有锁会导致其他需要此锁的线程挂起
-
多线程竞争下,加锁开锁会导致比较多的上下文切换 和 调度延时,引起性能问题。
-
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
-
数据安全有了一定的保证,但是可能引起死锁。