您的位置:首页 > 编程语言 > Java开发

《Java多线程编程核心技术》--第2章--对象及变量的并发访问

2018-01-26 15:38 441 查看
本章主要介绍Java多线程中的同步,也就是如何在Java语言中写出线程安全的程序,如何在Java语言中解决非线程安全的相关问题。多线程中的同步问题时学习多线程的重中之重。
本章重点:

synchronized对象监视器为Object时的使用
synchronized对象监视器为Class时的使用
非线程安全是如何出现的
关键字volatile的主要作用
关键字volatile和synchronized的区别及使用情况

一、synchronized同步方法

“非线程安全”产生的后果是“脏读”,也就是取到的数据其实是被更改过的。而“线程安全”就是已获得的实例变量的值是经过同步处理的,不会出现脏读的现象。

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(2000);
} 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 numRef;

public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}

@Override
public void run() {
numRef.addI("b");
}
}
public class Client {
public static void main(String[] args) {
HasSelfPrivateNum numRef = new HasSelfPrivateNum();
ThreadA threadA = new ThreadA(numRef);
threadA.start();
ThreadB threadB = new ThreadB(numRef);
threadB.start();
}
}
我们可以看出,线程A修改变量num之后休眠了2秒。在线程A休眠结束之前,线程B修改了变量num。最后,它们输出了各自修改的值。可以看出,它们修改的不是同一个变量。因为,方法内部的变量是线程私有的。

1.2 实例变量非线程安全

修改一下上面的程序,我们把变量调整为实例变量: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(2000);
} 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 numRef;

public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}

@Override
public void run() {
numRef.addI("b");
}
}
public class Client {
public static void main(String[] args) {
HasSelfPrivateNum numRef = new HasSelfPrivateNum();
ThreadA threadA = new ThreadA(numRef);
threadA.start();
ThreadB threadB = new ThreadB(numRef);
threadB.start();
}
}



A线程修改了num,期望值是100。修改之后,它休眠2秒,在休眠结束之前,B线程修改了num=200.最后,a,b线程输出的值都是200,说明,a,b线程修改了同一个变量。这就是线程不安全问题。
解决办法:同步方法
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) {
e.printStackTrace();
}
}
}

1.3 多个对象多个锁

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.print
e8a3
ln("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 numRef;

public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}

@Override
public void run() {
numRef.addI("b");
}
}
public class Client {
public static void main(String[] args) {
HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();
ThreadA threadA = new ThreadA(numRef1);
threadA.start();
ThreadB threadB = new ThreadB(numRef2);
threadB.start();
}
}



这个例子是两个线程分别访问同一个类的两个不同势力的相同名称的同步方法,效果却是以异步的方式运行的。本例由于创建了2个业务对象,在系统中产生出2个锁,所以运行结果是异步的,打印的效果就是先打印b,然后打印a。从上面程序运行结果来看,虽然在HasSelfPrivateNum.java中使用了synchronized关键字,但打印的顺序却不是同步的,是交叉的。为什么是这样的结果呢?
关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁,所以在上面的示例中,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能等待状态,前提是多个线程访问的是同一个对象。
但如果多个线程访问多个对象,则JVM会创建多个锁。上面的示例就是创建了2个HasSelfPrivateNum.java类的对象,所以就会产生出2个锁。

1.4 synchronized方法与锁对象

我们来看一下,同步的时候,锁是锁住方法,还是锁住对象。public class MyObject {
synchronized public void methodA() {
try {
System.out.println("begin methodA threadName=" + Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("end time=" + 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();
}
}
}
public class ThreadA extends Thread {
private MyObject myObject;
public ThreadA(MyObject myObject) {
this.myObject = myObject;
}
@Override
public void run() {
myObject.methodA();
}
}
public class ThreadB extends Thread {
private MyObject myObject;
public ThreadB(MyObject myObject) {
this.myObject = myObject;
}
@Override
public void run() {
myObject.methodB();
}
}
public class Client {
public static void main(String[] args) {
MyObject myObject = new MyObject();
ThreadA threadA = new ThreadA(myObject);
threadA.setName("a");
ThreadB threadB = new ThreadB(myObject);
threadB.setName("b");
threadA.start();
threadB.start();
}
}



试验可知,虽然线程A先持有了object对象的锁,但线程B完全可以异步调用非synchronized类型的方法。继续试验,将MyObject.java文件中的methodB()方法前加上synchronized关键字public class MyObject {
synchronized public void methodA() {
try {
System.out.println("begin methodA threadName=" + Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("end time=" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}

synchronized 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();
}
}
}


本示例是两个线程访问同一个对象的两个同步的方法。此试验的结论是:
A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。
A线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法则需等待,也就是同步。
synchronized锁住的是代码还是对象

1.5 脏读

上一节已经实现了多个线程调用同一个方法时,为了避免数据出现交叉的情况,使用synchronized关键字来进行同步。
虽然在赋值进行了同步,但在取值时有可能出现一些意想不到的意外,这种情况就是脏读(dirtyRead)。发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过了。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) {
e.printStackTrace();
}
}

public void getValue() {
System.out.println("getValue method thread name=" + Thread.currentThread().getName() + " username=" + username + " password=" + password);
}
}同步方法setValue()的锁属于类PublicVar的实例。
public class ThreadA extends Thread {
private PublicVar publicVar;

public ThreadA(PublicVar publicVar) {
this.publicVar = publicVar;
}

@Override
public void run() {
publicVar.setValue("B", "BB");
}
}
public class T3Test {
public static void main(String[] args) {
try {
PublicVar publicVar = new PublicVar();
ThreadA threadA = new ThreadA(publicVar);
threadA.start();
Thread.sleep(200);
publicVar.getValue();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}



出现脏读是因为public void getValue()方法并不是同步的,所以可以在任意时候进行调用。解决办法是加上synchronized关键字。脏读一定会出现操作实例变量的情况下,这就是不同线程“争抢”实例变量的结果。

1.6 synchronized锁重入

关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。
可重入锁也支持在父子类继承的环境中。

1.7 出现异常,锁自动释放

1.8 同步不具有继承性

二、synchronized同步语句块

用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程必须等待较长时间。在这样的情况下可以使用synchronized同步语句块来解决。

2.1 synchronized方法的弊端

2.2 synchronized同步代码块的弊端

2.3 用同步代码块解决同步方法的弊端

2.4 一半异步,一半同步

2.5 synchronized代码块间的同步性

在使用synchronized(this)代码块时需要注意的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”是一个。

2.6 验证同步synchronized(this)代码块是锁定当前对象的

2.7 将任意对象作为对象监视器

Java还支持对“任意对象”作为“对象监视器”来实现同步的功能。这个“任意对象”大多数是实例变量及方法的参数。
锁非this对象具有一定的优点:如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但会受到阻塞,所以影响运行效率;但如果使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,则可以大大提高运行效率。

2.8 细化验证3个结论

2.9 静态同步synchronized方法与synchronized(class)代码块

2.10 数据类型String的常量池特性

2.11 同步synchronized方法无闲等待与解决

2.12 多线程的死锁

2.13 内置类与静态内置类

2.14 内之类与同步:试验1

2.15 内之类与同步:试验2

2.16 锁对象的改变

三、volatile关键字

3.1 关键字volatile与死循环

3.2 解决同步死循环

3.3 解决异步死循环

3.4 volatile非原子的特性

3.5 使用原子类进行i++操作

3.6 原子类也并不完全安全

3.7 synchronized代码块有volatile同步的功能

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: