本文是我学习Java多线程以及高并发知识的第一本书的学习笔记,
书名是<<Java多线程编程核心技术>>,作者是大佬企业高级项目经理
高洪岩前辈,在此向他致敬。我将配合开发文档以及本书和其他的博客
奉献着的文章来学习,同时做一些简单的总结。有些基础的东西我就不
细分析了,建议以前接触过其他语言多线程或者没有系统学习过多线程
的开发者来看。另外需要注意的是,博客中给出的一些英文文档我就简单
翻译了,重要的部分会详细翻译,要不太浪费时间了,这个是我想提高
自己的英文阅读水平和文档查看能力,想要积攒内功的人可以用有谷歌
翻译自己看文档细读。(中文文档建议只参考,毕竟你懂得...)
详细代码见:github代码地址
本节内容:
1) synchronized同步方法(线程安全和非线程安全、锁对象)
2) 脏读
3) synchronized锁重入
我们在学习了Thread类的一些基本方法的使用以及了解了一些方法废弃的原因之后,接下
来我们来学习对对象及变量的并发访问。
1. synchronized同步方法
什么是线程安全与非线程安全?
"非线程安全":
会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是"脏读"
,也就是取到的数据是被更改过的。
"线程安全":
以获得的实例变量的值是经过同步处理的,不会出现脏读的现象。如果是方法内部的私有变量,则不存在"非线程安全"问题,所得结果也就是"线程安全"了。
这是因为,每个线程都拥有一个方法调用栈(线程栈),用来跟踪线程运行中的一系列的方法
调用过程。每当线程调用一个方法的时候,就会向方法栈中压入一个新帧。帧的内容是方法的
参数、局部变量、返回地址和运算过程中产生的临时数据。因此不存在"非线程安全"问题。
1) 为了证明实现方法内部声明一个变量时,是不存在"非线程安全"问题的,来举个例子:
package chapter02.section01.thread_2_1_1.project_1_t1;
public class HasSelfPrivateNum {
//private int num = 0;
public void addI(String username) {
try {
int num = 0;
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: handle exception
e.printStackTrace();
}
}
}
package chapter02.section01.thread_2_1_1.project_1_t1;
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");
}
}
package chapter02.section01.thread_2_1_1.project_1_t1;
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");
}
}
package chapter02.section01.thread_2_1_1.project_1_t1;
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum numRef = new HasSelfPrivateNum();
ThreadA athread = new ThreadA(numRef);
athread.start();
ThreadB bthread = new ThreadB(numRef);
bthread.start();
}
}
/*
result:
a set over!
b set over!
b num=200
a num=100
*/
结果分析:
方法中的变量不存在非线程安全问题,永远都是线程安全的。这是方法内部的变量是私有的
特性造成的。
2) 如果多个线程共同访问一个对象的实例变量,则有可能出现"非线程安全"问题
如果对象仅有一个实例变量,则可能出现覆盖的情况。
只需要将上述程序中注释去掉,addI()方法中的局部变量注释掉,则会出现错误。
/*
result:
a set over!
b set over!
b num=200
a num=200
解决数据不同步问题的方法是在方法前加上synchronized.
result:
a set over!
a num=100
b set over!
b num=200
*/
结果分析:
本来a线程将num设置为10,但是b线程期间设置为200,则a线程重新读取的时候就是200,出
现了数据混乱。
本实验是两个线程同时访问一个没有同步的方,如果两个线程同时操作业务对象中的实例变
量,则有可能出现"非线程安全"问题。只需要在public void addI(String name)方法前
加关键字sychronized即可。
3) 多个对象多个锁
举个例子:
package chapter02.section01.thread_2_1_3.project_twoObjectTwoLock;
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: handle exception
e.printStackTrace();
}
}
}
package chapter02.section01.thread_2_1_3.project_twoObjectTwoLock;
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");
}
}
package chapter02.section01.thread_2_1_3.project_twoObjectTwoLock;
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");
}
}
package chapter02.section01.thread_2_1_3.project_twoObjectTwoLock;
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();
}
}
/*
result:
a set over!
b set over!
b num=200
a num=100
*/
结果分析:
两个线程分别访问同一个类的两个不同的实例的相同名称的同步方法,效果是异步执行。关键
字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当做锁,因此在上述
示例中,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的
锁Lock,那么其他线程只能呈等待状态,前体是多个线程访问的是同一个对象。
但如果多个线程访问多个对象,则JVM会创建多个锁,因此两个线程分别拿到的是各自的对象锁。
4) synchronized方法和锁对象
举个例子:
package chapter02.section01.thread_2_1_4.project_1_synchronizedMethodLockObject;
public class MyObject {
synchronized public void methodA() {
// public void methodA() {
try {
System.out.println("begin methodA threadName="
+ Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("end");
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
package chapter02.section01.thread_2_1_4.project_1_synchronizedMethodLockObject;
public class ThreadA extends Thread {
private MyObject object;
public ThreadA(MyObject object) {
super();
this.object = object;
}
@Override
public void run() {
super.run();
object.methodA();
}
}
package chapter02.section01.thread_2_1_4.project_1_synchronizedMethodLockObject;
public class ThreadB extends Thread {
private MyObject object;
public ThreadB(MyObject object) {
super();
this.object = object;
}
@Override
public void run() {
super.run();
object.methodA();
}
}
package chapter02.section01.thread_2_1_4.project_1_synchronizedMethodLockObject;
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();
}
}
/*
方法不加synchronized关键字
两个线程可以同一进入methodA方法
result:
begin methodA threadName=B
begin methodA threadName=A
end
end
加synchronized关键字
排队进入方法
result:
begin methodA threadName=A
end
begin methodA threadName=B
end
*/
结果分析: 多个线程访问的是同一个对象,调用用关键字synchronized声明的方法一定是
排队运行的。
注:
另外只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本就没有同步的必
要。
如果两个线程调用的方法不一样,会有什么效果呢?
举个例子:
package chapter02.section01.thread_2_1_4.project_1_synchronizedMethodLockObject2;
public class MyObject {
synchronized public void methodA() {
try {
System.out.println("begin methodA threadName="
+ Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("end");
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
}
synchronized public void methodB() {
// 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) {
// TODO: handle exception
e.printStackTrace();
}
}
}
package chapter02.section01.thread_2_1_4.project_1_synchronizedMethodLockObject2;
public class ThreadA extends Thread {
private MyObject object;
public ThreadA(MyObject object) {
super();
this.object = object;
}
@Override
public void run() {
super.run();
object.methodA();
}
}
package chapter02.section01.thread_2_1_4.project_1_synchronizedMethodLockObject2;
public class ThreadB extends Thread {
private MyObject object;
public ThreadB(MyObject object) {
super();
this.object = object;
}
@Override
public void run() {
super.run();
object.methodB();
}
}
package chapter02.section01.thread_2_1_4.project_1_synchronizedMethodLockObject2;
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();
}
}
/*
methodB方法不加synchronized关键字result:
begin methodA threadName=A
begin methodB threadName=B begin time=1539504218450
end
end
methodB方法加synchronized关键字result:
begin methodA threadName=A
end
begin methodB threadName=B begin time=1539504295420
end
*/
结果分析:
methodB方法不加synchronized关键字情况下,虽然线程A先持有了object对象的锁,但线
程B完全可以异步调用非synchronized类型的方法;而methodB方法加上的话,A线程先持有
object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法则需
等待,也就是同步。
5) 脏读
脏读(dirtyRead):
为实现多个线程调用同一个方法时,为了避免数据出现交叉问题的情况。使用synchronized关
键字来进行同步。虽然在赋值时进行了同步,但在取值时有可能出现一些意想不到的意外,这种
情况就是脏读。发生脏读的情况是在读取实例变量时,此值已经被其他线程修改过了。脏读一定
会出现操作实例变量的情况下,这就是不同线程"争抢"实例变量的结果。
举个例子:
package chapter02.section01.thread_2_1_5.project_1_t3;
public class PublicVar {
public String username = "A";
public String password = "AA";
synchronized public void setValue(String username,
String password) {
try {
this.username = username;
Thread.sleep(5000);
this.password = password;
System.out.println("setValue method thread name="
+ Thread.currentThread().getName() + " username="
+ username + " password=" + password);
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
}
synchronized public void getValue() {
// public void getValue() {
System.out.println("getValue method thread name="
+ Thread.currentThread().getName() + " username=" + username
+ " password=" + password);
}
}
package chapter02.section01.thread_2_1_5.project_1_t3;
public class ThreadA extends Thread{
private PublicVar publicVar;
public ThreadA(PublicVar publicVar) {
super();
this.publicVar = publicVar;
}
@Override
public void run() {
super.run();
publicVar.setValue("B", "BB");
}
}
package chapter02.section01.thread_2_1_5.project_1_t3;
public class Test {
public static void main(String[] args) {
try {
PublicVar publicVarRef = new PublicVar();
ThreadA thread = new ThreadA(publicVarRef);
thread.start();
Thread.sleep(200); //打印结果受到此值影响,200<5000,因此会出现脏读
publicVarRef.getValue(); //主线程读取对象的实例变量出现了脏读
} catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
/*
PublicVar类的getValue()方法不加synchronized关键字的result:
getValue method thread name=main username=B password=AA
setValue method thread name=Thread-0 username=B password=BB
PublicVar类的getValue()方法加synchronized关键字的result:
setValue method thread name=Thread-0 username=B password=BB
getValue method thread name=main username=B password=BB
*/
结果分析:
getValue()方法加synchronized关键字之后,线程A调用publicVar对象的setValue(),
获得publicVar对象锁,这时候主线程main不能执行publicVarRef.getValue(),因为
它没有获得对象锁,A线程执行完之后才会释放pulicVar对象锁,main才能执行,因此看
到是排队执行。当A线程执行完run方法之后,username,password都已经被赋值,不存在
脏读的基本环境。
6) synchronized锁重入
什么是锁重入?
关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到
一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个synchronized
方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。
举个例子:
package chapter02.section01.thread_2_1_6.project_1_synLockIn_l;
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");
}
}
package chapter02.section01.thread_2_1_6.project_1_synLockIn_l;
public class MyThread extends Thread {
@Override
public void run() {
Service service = new Service();
service.service1();
}
}
package chapter02.section01.thread_2_1_6.project_1_synLockIn_l;
public class Run {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
/*
result:
service1
service2
service3
*/
"可以重入锁"的概念是: 自己可以再次获取自己的内部锁。比如有一条线程获得了某个对象的
锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的。
可重入锁也支持在父子类继承的环境中
举个例子:
package chapter02.section01.thread_2_1_6.project_2_synLockIn_2;
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: handle exception
e.printStackTrace();
}
}
}
package chapter02.section01.thread_2_1_6.project_2_synLockIn_2;
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) {
// TODO: handle exception
e.printStackTrace();
}
}
}
package chapter02.section01.thread_2_1_6.project_2_synLockIn_2;
public class MyThread extends Thread{
@Override
public void run() {
Sub sub = new Sub();
sub.operateISubMethod();
}
}
package chapter02.section01.thread_2_1_6.project_2_synLockIn_2;
public class Run {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
/*
result:
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
*/
结论: 当存在父子类继承关系时,子类是完全可以通过"可重入锁"调用父类的同步方法的。