前言
非线程安全会出现在多个线程同时对同一个对象的实例变量进行并发访问时发生,产生的后果就是脏读,也就是读取到的数据其实是被更改过的,而线程安全就是获得的实例变量是经过同步处理过的,不会出现脏读的情况.
本问主要讲解使用synchronized关键字实现同步,Java中还可以使用ReentrantLock同步,这将在另一篇文章中进行讲解Java的Lock同步.
synchronized同步方法
非线程安全的情况
package org.sync;
import java.util.Set;
public class AddSync {
private String name;
public static void main(String[] args) {
AddSync sync = new AddSync();
Thread1 thread1 = new Thread1(sync,"aaa");
Thread1 thread2 = new Thread1(sync,"bbb");
thread1.start();
thread2.start();
}
public void setName(String name) {
this.name = name;
try {
//模拟长时间的任务
Thread.sleep(500);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName() + " 显示的 name = " + this.name);
}
}
class Thread1 extends Thread{
AddSync sync;
private String name;
public Thread1(AddSync sync, String name) {
super();
this.sync = sync;
this.name = name;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 写入的 name = " + name);
sync.setName(name);
}
}
输出
可以看出来,写入的name和显示的不一样,两个线程并发访问了setName();方法,导致出现非线程安全.
Thread-0 写入的 name = aaa
Thread-1 写入的 name = bbb
Thread-0 显示的 name = bbb
Thread-1 显示的 name = bbb
在setName()上加上synchronized进行同步
synchronized public void setName(String name) {
this.name = name;
try {
Thread.sleep(500);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName() + " 显示的 name = " + this.name);
}
输出
加上synchronized,程序对方法setName()进行了同步处理,每次只能有一个线程进行访问,此时不会再出现线程不安全问题.
Thread-1 写入的 name = bbb
Thread-0 写入的 name = aaa
Thread-1 显示的 name = bbb
Thread-0 显示的 name = aaa
synchronized是对象锁
验证synchronized是对象锁
下面创建一个例子
1.多个线程访问单个实例
2.多个线程访问不同的实例
package org.sync;
/**
* 验证多个对象多个锁
* @author lgj
*
*/
public class ObjSync extends Thread {
private String name;
public static void main(String[] args) {
//多个线程访问同一对象
System.out.println("多个线程访问同一对象");
//创建一个对象
ObjSync obj1 = new ObjSync("aaa");
Thread[] thread = new Thread[5];
//创建5个线程
for(int i = 0;i<5;i++) {
thread[i] = new Thread(obj1);
}
//启动线程
for(int i = 0;i<5;i++) {
thread[i].start();
}
//等待上面的程序执行完
try {
Thread.sleep(2000);
} catch (Exception e) {
// TODO: handle exception
}
//多个线程访问不同对象
System.out.println("多个线程访问不同对象");
ObjSync[] obj = new ObjSync[5];
//创建5个对象
for(int i = 0;i<5;i++) {
obj[i] = new ObjSync(""+i);
}
//创建5个线程
Thread[] thread1 = new Thread[5];
for(int i = 0;i<5;i++) {
thread1[i] = new Thread(obj[i]);
}
for(int i = 0;i<5;i++) {
thread1[i].start();
}
}
public ObjSync(String name) {
super();
this.name = name;
}
@Override
synchronized public void run() {
System.out.println("线程:" + Thread.currentThread().getName() + " 实例类:" + this.name + "开始执行线程");
try {
Thread.sleep(200);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println("线程:" + Thread.currentThread().getName() + " 实例类:" + this.name + "开始退出线程");
}
}
1.多个线程访问单个实例
每一个线程都要等待其他获得锁的线程执行完后才能执行.
2.多个线程访问不同的实例
由于是不同的对象,因此所有的线程都是同步执行,互不影响.
由以上可知,synchronized同步非静态方法是对象锁,不同的对象互不影响.
多个线程访问同一对象
线程:Thread-1 实例类:aaa开始执行线程
线程:Thread-1 实例类:aaa开始退出线程
线程:Thread-2 实例类:aaa开始执行线程
线程:Thread-2 实例类:aaa开始退出线程
线程:Thread-5 实例类:aaa开始执行线程
线程:Thread-5 实例类:aaa开始退出线程
线程:Thread-4 实例类:aaa开始执行线程
线程:Thread-4 实例类:aaa开始退出线程
线程:Thread-3 实例类:aaa开始执行线程
线程:Thread-3 实例类:aaa开始退出线程
多个线程访问不同对象
线程:Thread-11 实例类:0开始执行线程
线程:Thread-13 实例类:2开始执行线程
线程:Thread-14 实例类:3开始执行线程
线程:Thread-15 实例类:4开始执行线程
线程:Thread-12 实例类:1开始执行线程
线程:Thread-11 实例类:0开始退出线程
线程:Thread-13 实例类:2开始退出线程
线程:Thread-14 实例类:3开始退出线程
线程:Thread-15 实例类:4开始退出线程
线程:Thread-12 实例类:1开始退出线程
synchronized重入锁
在上面的基础上增加两个方法
/**
* 测试可重入锁
*/
synchronized public void method1() {
System.out.println("method1执行");
method2();
}
synchronized public void method2() {
System.out.println("method2执行");
}
//run()方法
@Override
public void run() {
method1();
}
测试
ObjSync thObjSync = new ObjSync("aaa");
thObjSync.start();
输出
method1执行
method2执行
在method1()中,线程继续访问同步方法method2().并且成功访问.假如锁是不可重入的,那么线程访问method2()将会失败,因为没有获得锁.这说明synchronized的锁是可重入的.
脏读
当写进行了同步,但是读没有进行同步,有可能就会出现读取的值与写入的值不一致.发生脏读的情况是读取实例变量时,此值已经被其他线程改过了.
出现异常,锁自动释放
当出现异常时,线程持有的对象锁自动释放,并且该线程结束执行.
同步不具有继承性
当子类重写父类的方法,并且父类中的方法已经使用synchronized进行同步,如果子类重写的方法也需要实现同步,仍需要加上synchronized.
synchronized同步方法的问题
synchronized同步方法会降低程序的效率.
如下的一个同步方法,假如只有task10ms();需要进行同步,当有10个线程进行访问时,因为每个线程只有获得锁之后才能执行,因此总的运行时间大约是110ms * 10 = 1100ms.
下一节使用synchronized同步语句块来解决这个问题.
synchronized public void method(){
//模拟一个100ms的任务
task100ms();
//继续执行一个10ms的任务
task10ms();
}
synchronized同步语句块
返回目录
继续上节的问题
这回只对task10ms();进行同步,当有10个线程进行访问时,task100ms();可以同时执行,而task10ms();需要获得锁之后才能执行,因此总的执行时间是100ms+10*10ms=200ms,效率提高了很多.
public void method(){
//模拟一个100ms的任务
task100ms();
//继续执行一个10ms的任务
synchronized(this){
task10ms();
}
}
synchronized(this)同步语句块获得的是对象锁
package org.sync.block;
/**
* 验证synchronized同步语句块获得的是对象锁
* @author lgj
*
*/
public class BlockSync {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyBlockSync myBlockSync = new MyBlockSync();
MyThread1 th1 = new MyThread1(myBlockSync);
MyThread2 th2 = new MyThread2(myBlockSync);
MyThread3 th3 = new MyThread3(myBlockSync);
MyThread4 th4 = new MyThread4(myBlockSync);
th1.start();
th2.start();
th3.start();
//th4.start();
}
}
class MyBlockSync{
Object obj = new Object();
//method1是同步方法
synchronized public void method1() {
System.out.println("执行同步方法-method1");
System.out.println("+++进入同步方法-method1");
try {
Thread.sleep(2000);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println("---退出同步方法-method1\r\n");
}
//method2是同步语句块
public void method2() {
System.out.println("执行同步代码块-method2");
synchronized(this) {
try {
System.out.println("+++进入同步块-method2");
Thread.sleep(500);
} catch (Exception e) {
// TODO: handle exception
}
}
System.out.println("---退出同步代码块-method2\r\n");
}
//method3是同步语句块
public void method3() {
System.out.println("执行同步代码块-method3");
synchronized(this) {
try {
System.out.println("+++进入同步块-method3");
Thread.sleep(500);
} catch (Exception e) {
// TODO: handle exception
}
}
System.out.println("---退出同步代码块-method3\r\n");
}
}
/**
* 线程类
* @author lgj
*
*/
class MyThread1 extends Thread{
MyBlockSync myBlockSync;
public MyThread1(MyBlockSync myBlockSync) {
super();
this.myBlockSync = myBlockSync;
}
@Override
public void run() {
//调用method1
myBlockSync.method1();
}
}
class MyThread2 extends Thread{
MyBlockSync myBlockSync;
public MyThread2(MyBlockSync myBlockSync) {
super();
this.myBlockSync = myBlockSync;
}
@Override
public void run() {
//调用method2
myBlockSync.method2();
}
}
class MyThread3 extends Thread{
MyBlockSync myBlockSync;
public MyThread3(MyBlockSync myBlockSync) {
super();
this.myBlockSync = myBlockSync;
}
@Override
public void run() {
//调用method3
myBlockSync.method3();
}
}
输出
执行同步方法-method1
+++进入同步方法-method1
执行同步代码块-method3
执行同步代码块-method2
---退出同步方法-method1
+++进入同步块-method2
---退出同步代码块-method2
+++进入同步块-method3
---退出同步代码块-method3
method1()睡眠了2000ms,线程2和3都是等其执行完才获得锁,因此synchronized(this)同步语句块和synchronized同步方法获得的都是对象锁.
将任意对象作为对象监视器synchronized(非this对象)
上面是使用synchronized(this)来同步代码块,Java还支持非this对象作为对象监视器来实现同步.
Object obj = new Object();
public void func(){
synchronized(obj){
//语句块
}
}
在上面的例子中添加如下代码
//创建一个对象作为对象监视器
Object obj = new Object();
//添加两个同步语句块,使用非this对象作为对象监视器
//method4是同步语句块
public void method4() {
System.out.println("执行同步代码块-method4");
synchronized(obj) {
try {
System.out.println("+++进入同步块-method4");
Thread.sleep(2000);
} catch (Exception e) {
// TODO: handle exception
}
}
System.out.println("---退出同步代码块-method4\r\n");
}
//method5是同步语句块
public void method5() {
System.out.println("执行同步代码块-method5");
synchronized(obj) {
try {
System.out.println("+++进入同步块-method5");
Thread.sleep(2000);
} catch (Exception e) {
// TODO: handle exception
}
}
System.out.println("---退出同步代码块-method5\r\n");
}
//添加两个线程
class MyThread4 extends Thread{
MyBlockSync myBlockSync;
public MyThread4(MyBlockSync myBlockSync) {
super();
this.myBlockSync = myBlockSync;
}
@Override
public void run() {
//调用method4
myBlockSync.method4();
}
}
class MyThread5 extends Thread{
MyBlockSync myBlockSync;
public MyThread5(MyBlockSync myBlockSync) {
super();
this.myBlockSync = myBlockSync;
}
@Override
public void run() {
//调用method4
myBlockSync.method5();
}
}
//测试
//非this对象作为同步监视器
MyThread4 th4 = new MyThread4(myBlockSync);
MyThread5 th5 = new MyThread5(myBlockSync);
th4.start();
th5.start();
输出
执行同步代码块-method3
执行同步代码块-method4
+++进入同步块-method4
执行同步代码块-method5
---退出同步方法-method1
+++进入同步块-method3
---退出同步代码块-method4
+++进入同步块-method5
---退出同步代码块-method3
+++进入同步块-method2
---退出同步代码块-method2
---退出同步代码块-method5
从以上输出可以看出:
1.synchronized(非this对象)获得的锁不是对象锁,与synchronized(this)和synchronized同步方法不互相阻塞.
2.synchronized(非this对象),如果对象为同一个对象,那么同一时间只能有一个线程能够访问.
静态方法使用synchronized
在上述例程中添加如下代码
//静态同步方法
//method6是静态同步方法
synchronized public static void method6() {
System.out.println("+++进入静态同步方法-method6");
try {
Thread.sleep(2000);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println("---退出静态同步方法-method6\r\n");
}
//method7是静态同步方法
synchronized public static void method7() {
System.out.println("+++进入静态同步方法-method7");
try {
Thread.sleep(2000);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println("---退出静态同步方法-method7\r\n");
}
//线程
class MyThread6 extends Thread{
MyBlockSync myBlockSync;
public MyThread6(MyBlockSync myBlockSync) {
super();
this.myBlockSync = myBlockSync;
}
@Override
public void run() {
//调用method4
myBlockSync.method6();
}
}
class MyThread7 extends Thread{
MyBlockSync myBlockSync;
public MyThread7(MyBlockSync myBlockSync) {
super();
this.myBlockSync = myBlockSync;
}
@Override
public void run() {
//调用method4
myBlockSync.method7();
}
}
//测试
//静态同步方法
MyThread6 th6 = new MyThread6(myBlockSync);
MyThread7 th7 = new MyThread7(myBlockSync);
//静态同步方法
th6.start();
th7.start();
输出
执行同步代码块-method2
执行同步方法-method1
+++进入同步方法-method1
执行同步代码块-method4
+++进入同步块-method4
执行同步代码块-method3
执行同步代码块-method5
+++进入静态同步方法-method6
---退出同步方法-method1
+++进入同步块-method5
---退出静态同步方法-method6
+++进入静态同步方法-method7
---退出同步代码块-method4
+++进入同步块-method3
---退出同步代码块-method3
+++进入同步块-method2
---退出同步代码块-method2
---退出同步代码块-method5
---退出静态同步方法-method7
从以上可以看出
1.静态同步方法获得的是类锁
2.(静态同步方法),(非静态同步方法和 synchronized(this) 同步语句块), synchronized(非this对象)这三者获取的都是不同的锁,不会互相阻塞.
查看线程死锁
public void method2() {
System.out.println("执行同步代码块-method2");
synchronized(this) {
System.out.println("+++进入同步块-method2");
while(true);
}
}
//method3是同步语句块
public void method3() {
System.out.println("执行同步代码块-method3");
synchronized(this) {
System.out.println("+++进入同步块-method3");
while(true) {
}
}
}
执行 jps 命令,得到运行的线程PID为17055
再执行 jstack -l 17055,便可以获得结果,清晰地指明了哪个类的哪个方法出现死锁.