目录
1. 方法内的变量为线程安全
2. 实例变量非线程安全
3. 多个对象多个锁
4.?synchronized方法与锁对象
5. 脏读
6.?synchronized锁重入
7. 出现异常,锁自动释放
8. 同步不具有继承性
“非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取得的数据其实是被更改过的。
1. 方法内的变量为线程安全
“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题,所得结果也就是“线程安全”的了。
2. 实例变量非线程安全
如果多个线程共同访问1个对象中的实例变量,则有可能出现“非线程安全”问题。只需要在方法前加关键字synchronized即可。
两个线程访问同一个对象中的同步方法时一定是线程安全的。
3. 多个对象多个锁
先来看一个示例:
(1) HasSelfPrivateNum.java
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(2000);
} else {
num = 200;
System.out.println("b set over!");
}
System.out.println(username + " num=" + num);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
(2)?ThreadA.java
public class ThreadA extends Thread {
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
super();
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("a");
}
}
(3)?ThreadB.java
public class ThreadB extends Thread {
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
super();
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("b");
}
}
(4) 主类 Run.java
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();
ThreadA athread = new ThreadA(numRef1);
athread.start();
ThreadB bthread = new ThreadB(numRef2);
bthread.start();
}
}
运行结果如下:
a set over!
b set over!
b num=200
a num=100
上面的示例是两个线程分别访问同一个类的不同实例的相同名称的同步方法,效果确实以异步的方式运行的。
从上面程序的运行结果来看,虽然在HasSelfPrivateNum.java中使用了synchronized关键字,但打印的顺序却不是同步的,是交叉的。为什么是这样的结果呢?
关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法当作锁,所以在上面的示例中,哪个线程先执行带synchronized关键字的方法,就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。
但如果多个线程访问多个对象,则JVM会创建多个锁。
同步的单词为synchronized,异步的单词为asynchronized。
4.?synchronized方法与锁对象
上面3中的示例证明了线程锁的是对象。调用关键字synchronized声明的方法一定是排队运行的。另外需要牢牢记住“共享”这两个字,只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本没有同步的必要。
那其他方法在调用时会是什么效果呢?如何查看到Lock锁对象的效果呢?下面我们用一个示例来说明:
(1)MyObject.java
public class MyObject {
synchronized public void methodA() {
try {
System.out.println("begin methodA threadName="
+ Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("end endTime=" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void methodB() {
try {
System.out.println("begin methodB threadName="
+ Thread.currentThread().getName() + " begin time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
(2)?ThreadA.java
public class ThreadA extends Thread {
private MyObject object;
public ThreadA(MyObject object) {
super();
this.object = object;
}
@Override
public void run() {
super.run();
object.methodA();
}
}
(3)?ThreadB.java
public class ThreadB extends Thread {
private MyObject object;
public ThreadB(MyObject object) {
super();
this.object = object;
}
@Override
public void run() {
super.run();
object.methodB();
}
}
(4)?Run.java
public class Run {
public static void main(String[] args) {
MyObject object = new MyObject();
ThreadA a = new ThreadA(object);
a.setName("A");
ThreadB b = new ThreadB(object);
b.setName("B");
a.start();
b.start();
}
}
运行结果:
begin methodA threadName=A
begin methodB threadName=B begin time=1633888880077
end endTime=1633888885092
end
通过上面的实验可以得知,虽然线程A先持有了object对象的锁,但线程B完全可以异步调用非synchronized类型的方法。
继续实验,将MyObject.java文件中的methodB()方法前面加上synchronized关键字,本示例是两个线程访问同一个对象的两个同步的方法。运行结果为:
begin methodA threadName=A
end endTime=1633889057071
begin methodB threadName=B begin time=1633889057075
end
本小节的实验结论是:
(1)A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。
(2)A线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法则需要等待,也就是同步。
【注】:可以理解为,synchronized锁对象,会同时锁住该对象中所有synchronized类型的方法。
5. 脏读
有些程序虽然在赋值时进行了同步,但在取值时有可能出现一些意想不到额意外,这种情况就是“脏读”(dirtyRead)。
根据上面4中的结论可见,避免脏读的办法可以在setValue()和getValue()方法前同时加上synchronized关键字。
当A线程调用MyObject对象中加了synchronized关键字的X方法时,A线程就获得了A方法锁,确切地讲,是获得了对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,但B线程可以随意调用其他的非synchronized同步方法。
当A线程调用MyObject对象加入synchronized关键字时,A对象就获得了X方法所在对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,而B线程如果调用声明了synchronized关键字的非X方法时,必须等A线程将X方法执行完,而就是释放对象锁之后才可以调用。这是A线程已经执行了一个完整的任务。
脏读一定出现在操作实例变量的情况下,这就是不同线程“争抢”实例变量的结果。
6.?synchronized锁重入
关键字synchronized拥有锁重入的功能,也就是说在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。
【注】:意思就是,当一个线程得到一个对象锁之后,可以访问这个对象所有加synchronized的方法,当然也可以访问非synchronized方法。
可重入锁的概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象还没有释放,当其再次想获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
这也证明了在一个synchronized方法/块的内部调用本类的其他方法/块时,是永远可以得到锁的。
下面通过一个示例来说明:
(1)Service.java
public class Service {
synchronized public void service1() {
System.out.println("service1");
service2();
}
synchronized public void service2() {
System.out.println("service2");
service3();
}
synchronized public void service3() {
System.out.println("service3");
}
}
(2)?MyThread.java
public class MyThread extends Thread {
@Override
public void run() {
Service service = new Service();
service.service1();
}
}
(3) Run.java
public class Run {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
运行结果:
service1
service2
service3
可重入锁也支持在父子继承的环境中。
下面的示例证明说了这一点:
(1)Main.java
public class Main {
public int i = 10;
synchronized public void operateIMainMethod() {
try {
i--;
System.out.println("main print i=" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
(2) Sub.java
public class Sub extends Main {
synchronized public void operateISubMethod() {
try {
while (i > 0) {
i--;
System.out.println(" -- sub print i=" + i);
Thread.sleep(100);
this.operateIMainMethod();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
(3) MyThread.java
public class MyThread extends Thread {
@Override
public void run() {
Sub sub = new Sub();
sub.operateISubMethod();
}
}
(4) Run.java
public class Run {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
运行结果:
-- sub print i=9
main print i=8
-- sub print i=7
main print i=6
-- sub print i=5
main print i=4
-- sub print i=3
main print i=2
-- sub print i=1
main print i=0
此实验说明,当存在父子继承关系时,子类完全可以通过“可重入锁”调用父类的同步方法。
7. 出现异常,锁自动释放
当一个线程执行的代码出现异常时,其持有的锁会自动释放。
8. 同步不具有继承性
同步不可以继承。
下面通过一个示例来验证。
(1) Main.java
public class Main {
synchronized public void serviceMethod() {
try {
System.out.println("main " + Thread.currentThread().getName() + " sleep begin threadName="
+ " time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("main " + Thread.currentThread().getName() + " sleep end threadName="
+ " time="
+ System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
(2) Sub.java
public class Sub extends Main {
@Override
public void serviceMethod() {
try {
System.out.println(" -- sub " + Thread.currentThread().getName() + " sleep begin threadName="
+ " time = "
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println(" -- sub " + Thread.currentThread().getName() + " sleep end threadName="
+ " time="
+ System.currentTimeMillis());
super.serviceMethod();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
(3) MyThreadA.java
public class MyThreadA extends Thread {
private Sub sub;
public MyThreadA(Sub sub) {
super();
this.sub = sub;
}
@Override
public void run() {
sub.serviceMethod();
}
}
(4) MyThreadB.java
public class MyThreadB extends Thread {
private Sub sub;
public MyThreadB(Sub sub) {
super();
this.sub = sub;
}
@Override
public void run() {
sub.serviceMethod();
}
}
(5) Run.java
public class Run {
public static void main(String[] args) {
Sub subRef = new Sub();
MyThreadA a = new MyThreadA(subRef);
a.setName("A");
a.start();
MyThreadB b = new MyThreadB(subRef);
b.setName("B");
b.start();
}
}
运行结果:
-- sub B sleep begin threadName= time = 1633892050675
-- sub A sleep begin threadName= time = 1633892050675
-- sub A sleep end threadName= time=1633892055699
-- sub B sleep end threadName= time=1633892055699
main A sleep begin threadName= time=1633892055699
main A sleep end threadName= time=1633892060700
main B sleep begin threadName= time=1633892060700
main B sleep end threadName= time=1633892065701
由此示例可以看到,同步不能继承,所以还得在子类的方法中添加synchronized关键字。添加后的运行效果如下:
-- sub A sleep begin threadName= time = 1633892420973
-- sub A sleep end threadName= time=1633892425991
main A sleep begin threadName= time=1633892425991
main A sleep end threadName= time=1633892430991
-- sub B sleep begin threadName= time = 1633892430991
-- sub B sleep end threadName= time=1633892435991
main B sleep begin threadName= time=1633892435991
main B sleep end threadName= time=1633892440992
|