1 概述
关键字synchronzied保障了原子性、可见性和有序性。
非线程安全问题会在多个线程对同一个对象中的同一个实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是读取到的数据其实是被更改过的。而线程安全是指获取的实例变量的值是经过同步处理的,不会出现脏读的现象。本篇将细化线程并发访问的内容,在细节上更多讲解并发时变量值的处理方法。
2 方法内的变量是线程安全的
非线程安全问题存在时实例变量中,如果是方法内部私有变量,则不存在非线程安全问题,结果是线程安全的。
【示例1.1.1】演示方法内部声明一个变量时,是不存在“非线程安全”问题的。
public class HasSelfPrivateNum {
public void addI(String username){
try {
int num = 0;
if(username.equals("a")){
num = 100;
System.out.println("a set over");
Thread.sleep(4000);
}else{
num = 200;
System.out.println("b set over");
}
System.out.println(username + "num = " +num);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run(){
numRef.addI("a");
}
}
public class ThreadB extends Thread{
private HasSelfPrivateNum refNef ;
public ThreadB(HasSelfPrivateNum refNef) {
this.refNef = refNef;
}
@Override
public void run(){
refNef.addI("b");
}
}
public class RunDemo201 {
public static void main(String[] args) {
HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
ThreadA a = new ThreadA(hasSelfPrivateNum);
ThreadB b = new ThreadB(hasSelfPrivateNum);
a.start();
b.start();
}
}
3 实例变量“非线程安全”问题及解决方案
如果多个线程共同访问一个对象中的实例变量,则有可能出现非线程安全问题。线程访问的对象中如果有多个实例变量,则运行的结果有可能出现交叉的情况。把上面 HasSelfPrivateNum类中addI()方法的局部变量改为全局变量。在执行,num有可能被覆盖。
public class HasSelfPrivateNum {
private int num = 0;
public void addI(String username){
try {
if(username.equals("a")){
num = 100;
System.out.println("a set over");
Thread.sleep(4000);
}else{
num = 200;
System.out.println("b set over");
}
System.out.println(username + "num = " +num);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
这个实验是两个线程同时访问一个业务对象中的同一个没有同步的方法,如果两个线程同时操作业务对象中的实例变量,则有可能会出现非线程安全问题,根据前面的介绍,只需要在方法加上synchronized即可,更改后的代码:
public class HasSelfPrivateNum {
private int num = 0;
synchronized public void addI(String username){
try {
if(username.equals("a")){
num = 100;
System.out.println("a set over");
Thread.sleep(4000);
}else{
num = 200;
System.out.println("b set over");
}
System.out.println(username + "num = " +num);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
4 同步synchronzied在字节码指令中的原理
在方法上使用synchronzied关键字实现同步的原因是使用了flag标记ACC_SYNCHRONZIED,当调用方法时,调用质量会检查方法的ACC_SYNCHRONZIED访问标志是否设置,如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁。
【示例1.3.1】测试代码
public class TestSynchronzied {
synchronized public static void testMethod(){
}
public static void main(String[] args) throws InterruptedException{
testMethod();
}
}
使用javap命令把class文件转换成字节码指令,如下:
javap -c -v TestSynchronzied.class
生成这个class文件对应的字节码指令,指令的核心代码如下:
public com.jay.current.demo213.TestSynchronzied();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/jay/current/demo213/TestSynchronzied;
public static synchronized void testMethod();
descriptor: ()V
flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 6: 0
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #7 // Method testMethod:()V
3: return
LineNumberTable:
line 9: 0
line 10: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
Exceptions:
throws java.lang.InterruptedException
在反编译的字节码指令中对public synchronzied void testMethod()方法使用了flag标记ACC_SYNCHRONZIED,说明此方法时同步的。
如果使用synchronzied代码块,则使用monitorenter和monitorexit指令进行同步处理。测试代码如下:
public class TestSynchronzied_2 {
public void testMethod(){
synchronized (this){
int age = 100;
}
}
public static void main(String[] args) throws InterruptedException{
TestSynchronzied_2 testSynchronzied2 = new TestSynchronzied_2();
testSynchronzied2.testMethod();
}
}
在CMD执行命令:
javap -c -v TestSynchronzied_2.class
生成的字节码指令如下:
public void testMethod();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: bipush 100
6: istore_2
7: aload_1
8: monitorexit
9: goto 17
12: astore_3
13: aload_1
14: monitorexit
15: aload_3
16: athrow
17: return
Exception table:
from to target type
4 9 12 any
12 15 12 any
LineNumberTable:
line 5: 0
line 6: 4
line 7: 7
line 8: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 this Lcom/jay/current/demo213/TestSynchronzied_2;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 12
locals = [ class com/jay/current/demo213/TestSynchronzied_2, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #7 // class com/jay/current/demo213/TestSynchronzied_2
3: dup
4: invokespecial #9 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #10 // Method testMethod:()V
12: return
LineNumberTable:
line 11: 0
line 12: 8
line 13: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 args [Ljava/lang/String;
8 5 1 testSynchronzied2 Lcom/jay/current/demo213/TestSynchronzied_2;
Exceptions:
throws java.lang.InterruptedException
有代码可知,在字节码使用了monitorenter和monitorexit指令进行同步处理。
5 多个对象多个锁
【示例1.4.1】
public class HasSelfPrivateNum {
synchronized public void testMethod(){
try {
System.out.println(Thread.currentThread().getName() + " begin " + System.currentTimeMillis());
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " end " + System.currentTimeMillis());
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
上面的代码有同步方法testMethod(),说明此方法在正常情况下应该被顺序调用。在创建两个线程类。
public class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run(){
numRef.testMethod();
}
}
public class ThreadB extends Thread{
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run(){
numRef.testMethod();
}
}
最后创建main函数类
public class Run1 {
public static void main(String[] args) {
HasSelfPrivateNum num1 = new HasSelfPrivateNum();
HasSelfPrivateNum num2 = new HasSelfPrivateNum();
ThreadA a = new ThreadA(num1);
a.start();
ThreadB b = new ThreadB(num2);
b.start();
}
}
运行结果:
代码分析:首先创建了两个 HasSelfPrivateNum.java类的对象,即产生了两把锁。两个线程分别访问同一个类的两个不同示例的相同名称的同步方法(testMethod()),控制台输出了两个begin和end,且begin和end不是成对的输出,呈现了两个线程交叉输出的效果,说明两个线程以异步方式同时运行。
本示例创建了两个业务对象,在系统中产生了两把锁,线程和业务对象属于一对一的关系,每个线程执行自己所属业务对象中的方法,不存在锁的争抢关系,所以运行结果是异步的。另外,在这种情况下synchronzied可以不需要,因为不会出现非线程安全的问题。
只有多个线程执行统一个业务对象中的同步方法时,线程和业务对象属于多对一的关系,为了避免出现非线程安全问题,所以使用了synchronzied。
总结:多个线程对共享的资源有写操作,则必须同步,如果只是读操作,则不需要同步。
6 synchronzied方法将对象作为锁
为了证明上面说的将对象作为锁,开发以下代码:
public class MyObject {
public void methodA(){
try{
System.out.println("开始执行 methodA方法,线程名为:"+Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("end");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadADemo215 extends Thread{
private MyObject myObject;
public ThreadADemo215(MyObject myObject) {
this.myObject = myObject;
}
@Override
public void run(){
myObject.methodA();
}
}
public class ThreadBDemo215 extends Thread{
private MyObject myObject;
public ThreadBDemo215(MyObject myObject) {
this.myObject = myObject;
}
@Override
public void run(){
myObject.methodA();
}
}
public class Run1 {
public static void main(String[] args) {
MyObject object = new MyObject();
ThreadADemo215 a = new ThreadADemo215(object);
a.setName("A");
ThreadBDemo215 b = new ThreadBDemo215(object);
b.setName("B");
a.start();
b.start();
}
}
运行结果
两个线程一同进入methodA方法,因为该方法没有同步。更改MyObject类,加上synchronzied关键字。
public class MyObject {
synchronized public void methodA(){
try{
System.out.println("开始执行 methodA方法,线程名为:"+Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("end");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
运行结果:
通过上面的示例得到结论,调用关键字synchronzied声明的方法一定是排队运行。另外,需要记住“共享”这两个字,只有共享资源的写操作才需要同步,如果不是共享资源,是没有同步的必要的。 那其他方法被调用时会有什么效果呢?如何查看将对象作为锁的效果呢?重新修改MyObject.java类,新增一个没用synchronzied修改的方法methodB()
public class MyObject {
synchronized public void methodA(){
try{
System.out.println("开始执行 methodA方法,线程名为:"+Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("end");
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void methodB(){
try{
System.out.println("开始执行 methodB方法,线程名为:"+Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("end");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
同时修改 ThreadBDemo215.java,run方法调用methodB()。运行结果:
通过这个示例得知,虽然线程A先持有了object对象的锁,但线程B完全可以异步调用非synchronzied类型的方法。
在把MyObject.java中的methodB方法加上synchronzied关键字,执行结果如下:
两个线程访问同一个对象的两个同步方法。结论如下:
1、A现成先持有object对象的锁,B现成可以以异步的方式调用object对象中的非synchronzied类型的方法。
2、A现成先持有object对象的锁,B现成如果在这时调用object对象中的synchronzied类型的方法,则需要等待A线程释放锁。
3、在方法声明处添加sync并不是锁方法,而是锁当前类的对象。
4、在java中,只有将对象作为锁,并没有锁方法这种说法。
5、在Java中,锁就是对象,对象可以映射成锁,哪个线程拿到这把锁,哪个线程就可以执行这个对象中的synchronzied同步方法。
6、如果在X对象中使用了synchronzied关键词声明非静态方法,则X对象就被当成锁。
7 脏读与解决
在多个线程调用同一个方法时,为了避免数据出现交叉的情况,使用synchronzied关键字进行同步。虽然在赋值时进行了同步,但是在取值时有可能出现脏读。发生脏读的原因是在读取实例变量时,此值已经被其他线程更改过了。
【示例7.1】
public class PublicVar {
public String username = "A";
public String password = "B";
synchronized public void setValue(String username,String password){
try{
this.username = username;
Thread.sleep(2000);
this.password = password;
System.out.println("执行setValue()方法,线程名是: "+Thread.currentThread().getName() + " username = " + username + ";password = " + password);
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void getValue(){
System.out.println("getValue()方法,线程名是: "+Thread.currentThread().getName() + " username = " + username + ";password = " + password);
}
}
public class ThreadADemo216 extends Thread{
private PublicVar publicVar;
public ThreadADemo216(PublicVar publicVar) {
this.publicVar = publicVar;
}
@Override
public void run(){
publicVar.setValue("B","BB");
}
}
public class Test1 {
public static void main(String[] args) {
try {
PublicVar publicVar = new PublicVar();
ThreadADemo216 a = new ThreadADemo216(publicVar);
a.start();
Thread.sleep(2000);
publicVar.getValue();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
运行结果:
出现脏读是因为getValue方法并不是同步的,所以可以在任意时候进行调用,解决办法是加上synchronzied。
8 synchronzied锁重入
关键词synchronzied拥有重入锁的功能,即在使用synchronzied时,当一个线程得到一个对象锁后,再次请求此对象锁时可以再次得到该对象的锁。这也证明在一个synchronzied方法/块的内部调用本类的其他synchronzied方法/this块时,是永远可以得到锁的。
public class Service {
synchronized public void service1(){
System.out.println("service1");
service2();
}
synchronized private void service2() {
System.out.println("service2");
service3();
}
synchronized private void service3() {
System.out.println("service3");
}
}
public class MyThread extends Thread{
@Override
public void run(){
Service service = new Service();
service.service1();
}
}
public class Run1 {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
可重入锁是指自己可以再次获取自己的内部锁。例如,有1个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象锁时还可以获取。如果是不可重入锁,方法service2和service3都不会被调用。
9 出现异常,锁自动释放
当一个线程执行的代码出现异常时,其锁持有的锁会自动释放。
【示例】
public class Service {
synchronized public void testMethod(){
if(Thread.currentThread().getName().equals("a")){
System.out.println("线程名="+Thread.currentThread().getName()
+"开始执行时间 = " + System.currentTimeMillis());
int i = 1;
while(i == 1){
if(("" + Math.random()).substring(0,8).equals("0.123456")){
System.out.println("线程名="+Thread.currentThread().getName()
+"执行异常时间 = " + System.currentTimeMillis());
Integer.parseInt("a");
}
}
}else{
System.out.println("线程B运行时间 = " +System.currentTimeMillis());
}
}
}
创建两个自定义线程。
public class ThreadA extends Thread{
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run(){
service.testMethod();
}
}
public class ThreadB extends Thread{
private Service service;
public ThreadB(Service service) {
this.service = service;
}
@Override
public void run(){
service.testMethod();
}
}
public class Run1 {
public static void main(String[] args) {
try {
Service service = new Service();
ThreadA a = new ThreadA(service);
a.setName("a");
a.start();
Thread.sleep(500);
ThreadB b = new ThreadB(service);
b.setName("b");
b.start();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
线程a出现异常并释放锁,线程b进入方法正常输出,说明出现异常时,锁被自动释放了。
【注意】Thread.java中的suspend()和sleep()方法被调用后不会释放所。