我们先看一个示例 ,假如两个线程同时都对n进行自加操作
package Synchornized;
public class TestSynchornized0 {
int n = 0;
public void func(){
n++;
}
public static void main(String[] args) throws InterruptedException {
TestSynchornized0 testSynchornized0 = new TestSynchornized0();
Thread t1 = new Thread(){
@Override
public void run() {
for (int i = 0; i<5000; i++) {
testSynchornized0.func(); //线程1让n自加5000次
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
for (int i = 0; i<5000; i++) {
testSynchornized0.func(); //线程2也让n自加50000次
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(testSynchornized1.n); //第一次输出结果是9865
} //第二次输出结果是8520
}
我们的预期结果n的值应该是10000。但是结果确比10000少,且结果不确定,第一次执行是9865,第二次结果是8520。其实问题就出在两个线程同时修改一个变量,会造成线程不安全(比如n=0,两次线程同时从内存获取到n,分别进行自加后,两个线程的n就都是1,然后把n写入到内存,照我们的预想应该是2,但是结果是1,从结果来看就是少加了一次)。synchornized其实就是为了解决并发编程而诞生的产物,它可以让一个对象的一段代码,或者一个方法,同一时间只能有一个线程执行,另一个线程如果也要执行的话,就会阻塞在那,需要等正在执行的线程执行完了再执行。我们在下边的synchornized使用中会解决上边这个例子存在的问题
synchronized解决了可见性是因为synchronized每次加锁释放锁都会刷新工作内存,将更新完的数据写回到主内存中,然后从高内存中重新读取最新的数据
synchronized是Java提供的一个并发控制的关键字,作用于对象上。主要有两种用法,分别是同步方法(访问对象和clss对象)和同步代码块(需要加入对象),保证了代码的原子性和可见性以及有序性,但是不会处理重排序以及代码优化的过程,但是在一个线程中执行肯定是有序的,因此是有序的。
synchornized又被成为同步锁或者监视器锁
synchronized的底层是使用操作系统的mutex lock实现的。
当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量,这样保证了内存的可见性
synchronized用的锁是存在Java对象里面的
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入,这样保证了操作的原子性
其实synchornized同步锁/监视器锁中重点在于要搞清楚锁的对象,接下来我们分别看一下分别锁不同的对象的区别
synchornized如果用来修饰方法,就会让整个方法变成同步代码块。它锁的对象就是调用当前方法的对象,即两个线程调同一个对象的这个方法,就会造成线程阻塞。如果两个线程分别同时调用两个对象的方法,就不会造成阻塞,因为synchornized锁的是调用方法的对象。
package Synchornized;
import java.util.TreeMap;
public class TestSynchornized2 {
public synchronized void func() {
for (int i = 0; i < 10; i++) {
System.out.print(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
TestSynchornized2 testSynchornized2 = new TestSynchornized2();
Thread t1 = new Thread(){
@Override
public void run() { testSynchornized2.func(); }
};
Thread t2 = new Thread(){
@Override
public void run() { testSynchornized2.func(); }
};
t1.start(); t2.start();
t1.join(); t2.join();
}
}
调用一个对象的func方法程序执行结果
当我们把main函数改一下,改成两个线程调用两个对象的func方法
public static void main(String[] args) throws InterruptedException {
TestSynchornized2 ts1 = new TestSynchornized2();
TestSynchornized2 ts2 = new TestSynchornized2();
Thread t1 = new Thread(){
@Override
public void run() { ts1.func(); }
};
Thread t2 = new Thread(){
@Override
public void run() { ts2.func(); }
};
t1.start(); t2.start();
t1.join(); t2.join();
}
从结果对比我们可以看出来,第一次成功让线程阻塞,第二次确还是同步进行,为什么第二次没有阻塞呢?因为synchornized锁的是两个对象。执行的不是同一个方法,是两个对象的两个方法。由此可见,synchornized只能锁当前调用方法的对象。
public class TestSynchornized5 {
public static synchronized void func() {
for (int i = 0; i < 10; i++) {
System.out.print(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
TestSynchornized5 testSynchornized2 = new TestSynchornized5();
Thread t1 = new Thread(){
@Override
public void run() { testSynchornized2.func(); }
};
Thread t2 = new Thread(){
@Override
public void run() { testSynchornized2.func(); }
};
t1.start(); t2.start();
t1.join(); t2.join();
}
}
我们用两个对象去测试的时候
public static void main(String[] args) throws InterruptedException {
TestSynchornized5 testSynchornized1 = new TestSynchornized5();
TestSynchornized5 testSynchornized2 = new TestSynchornized5();
Thread t1 = new Thread(){
@Override
public void run() { testSynchornized1.func(); }
};
Thread t2 = new Thread(){
@Override
public void run() { testSynchornized2.func(); }
};
t1.start(); t2.start();
t1.join(); t2.join();
}
}
不管我们写一个对象还是两个对象,线程执行的收都出现了阻塞,其实当我们用synchornized修饰静态方法的时候,因为类的静态属性只有一个,所以就相当于我们锁了所有的实例对象,这和我们后边说的锁类对象是一样的。
synchornized锁代码块相比较于锁方法更加灵活方 便,因为代码块的范围可以自己控制,而锁方法是锁的整个方法,没法控制范围
其实synchornized锁this对象和锁方法是一样的,因为this就是当前对象。
package Synchornized;
public class TestSynchornized3 {
public void func() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
TestSynchornized3 testSynchornized3 = new TestSynchornized3();
Thread t1 = new Thread(){
@Override
public void run() { testSynchornized3.func(); }
};
Thread t2 = new Thread(){
@Override
public void run() { testSynchornized3.func(); }
};
t1.start(); t2.start();
t1.join(); t2.join();
}
}
我们可以看到结果是两个线程执行的时候出现了阻塞,和synchornized修饰方法的时候一样。
当我们把main函数改一下,改成两个线程调用两个对象的func方法
我们可以看我们两次运行的结果和修饰方法的时候一样,这也印证了我们之前的结论,synchornized修饰方法和修饰代码块this对象的效果是一样的。
不管是修饰一个普通方法还是修饰代码块的this对象,就好比工厂机器削苹果,假如给机器1和机器2都分配了一箱苹果,我们把方法上锁,就好比给每箱苹果都加了锁,一箱苹果同一时间只能由一个机器来削他,假如工人忘了给机器2放苹果,机器2想削机器1的苹果就得等机器1削完累了,然后再削。但是如果机器2分配了苹果,那两个机器就可以同时运转。因为两个机器削的是两箱苹果,互不干扰。
当我们不想把被锁对象写的范围太大,比如this啊,我们就可以去锁一个具体对象
public class TestSynchornized4 {
public void func(TestSynchornized4 test) { //传入要锁的对象
synchronized (test) {
for (int i = 0; i < 10; i++) {
System.out.print(i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
TestSynchornized4 test1 = new TestSynchornized4();
TestSynchornized4 test2 = new TestSynchornized4();
Thread t1 = new Thread(){
@Override
public void run() { test2.func(test1); } //第一个线程我们锁test1
};
t1.start();
Thread t2 = new Thread(){
@Override
public void run() { test2.func(test2); } //第一个线程我们锁test2
};
t2.start();
t1.join(); t2.join();
}
}
我们可以看到运行结果是两个进程同步进行,同一个对象调用同一个方法,为什么没有阻塞串行执行呢?就是因为我们两个传的参数不一样,锁的对象也不一样,所以就不会造成线程阻塞。依然是并行执行。当我们确定了被锁对象,我们就可以这么写
class Test
{
public void method( Object lock)
{
synchronized(lock) {
// 同步代码块
}
}
还有一种情况就是我们不确定锁的对象,但是想让一段代码同步,那我们就可以这么写
class Test
{
private Object lock = new Object;
public void method()
{
synchronized(lock) {
// 同步代码块
}
}
我们还是用削苹果来举例子,修饰具体对象的时候就相当于给某一箱特定的苹果加上锁,机器削其他箱苹果都可以几个机器同时削,但是当削到这一箱特殊的苹果的时候,就不行了,一个时间段只能只能有一个机器来削这箱苹果。
当我们锁类对象的时候,是这么写的
public class TestSynchornized6 {
public void func() {
synchronized (TestSynchornized6.class) {
for (int i = 0; i < 10; i++) {
System.out.print(i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
我们用和锁this对象一样的测试用例,结果不管是一个对象,还是两个对象,测试结果都是线程阻塞
其实我们用synchornized修饰静态方法,和修饰代码块(“类名.class”)是一样的效果,因为类对象和静态方法一样都是只有一个,所以不管哪个实例对象调用,调用的都是同一个方法,所以会造成线程阻塞。
这就相当于我们给工厂的存放苹果的仓库加了一个锁,不管有几台机器,只要这个机器削苹果,那就得排着队一个来。
总结
另外,还有一个需要注意的点就是,我们用synchornized修饰一个方法的时候,一个线程正在访问被修饰方法的时候,另一个线程依然可以调用这个对象没有被synchornized修饰的方法