第2章对象及变量的并发访问
标签: Java多线程编程
《Java多线程编程核心技术》 个人笔记
本章主要介绍Java多线程中的同步,写出线程安全的程序,解决非线程安全的相关问题
本章应该着重掌握如下技术点:
- synchronized对象监视器为Object是的使用
- synchronized对象监视器为Class时的使用
- 非线程安全是如何出现
- 非关键volatile的主要uzoyong
- 关键字volatile与synchronized的区别及使用情况
synchronized同步方法
- “非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据其实是被更改过的
- 而“线程安全”就是获得的实例对象的值是经过同步处理的,不会出现脏读现象
方法内的变量为线程安全
- “非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题,所得的结果是“线程安全”的。
实例变量非线程安全
- 如果多个线程共同访问1个对象中的实例变量,则可能出现“非线程安全”问题
- 用线程访问的对象中如果有多个实例变量,则运行的结果有肯出现交叉的情况(第一章演示过)
- 如果对象仅有1个实例变量,则有可能出现覆盖的情况(这个变量不是在方法内的局部变量,而是属于对象的私有变量),要解决只需要在方法的前面加关键字synchronized即可
多个对象多个锁
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();
}
}
- 两个线程分别访问同一个类的两个不同实例的相同名称的同步方法,效果却是异步的
- 关键字synchronized取得的锁是对象锁,而不是把一段代码或方法当作锁,
- 哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象
synchronized方法与锁对象
实验结论:调用关键字synchronized声明的方法一定是排队运行的。另外需要牢牢记住“共享”这两个字,只有共享资源的读写才需要同步化,如果不是共享资源,那么根本就没有同步的必要
对与同一个对象的不同方法:
public class MyObject {
synchronized public void methodA() { //保留synchronized
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() { //去掉synchronized做对比
synchronized public void methodB() {
try {
System.out.println("begin methodB threadName="
+ Thread.currentThread().getName() + " \nbegin time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//-------自定义线程A------
public class ThreadA extends Thread {
private MyObject object;
public ThreadA(MyObject object) {
super();
this.object = object;
}
@Override
public void run() {
super.run();
object.methodA(); //执行的是methodA()方法
}
}
//--------自定义线程B方法
public class ThreadB extends Thread {
private MyObject object;
public ThreadB(MyObject object) {
super();
this.object = object;
}
@Override
public void run() {
super.run();
object.methodB(); //执行的是methodB()方法
}
}
//----------主函数--------
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();
}
}
- 结果:methodA 一直保留synchronized,而methodB一次保留,另一次不保留,当methodB保留synchronized时,线程A和线程B执行的methodA 和methodB 为同步的; 当不保留时,是异步的
- 结论:
- A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法
- A线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的synchronize类型的方法则需要等待,即同步。
脏读
- 虽然在赋值时进行了同步,但在取值时有可能出现一些意想不到的意外,即“脏读 dirtyRead”
- 出现脏读是因为getValue()获取值时不是同步的,所以加上synchronized关键字即可解决。
synchronized锁重入
- 关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个synchronized方法/块内部调用本类的其他synchronized方法时,是永远可以得到锁的。
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");
}
}
//----------自定义线程类-----
public class MyThread extends Thread {
@Override
public void run() {
Service service = new Service();
service.service1();
}
}
//----------主函数-------
public class Run {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
- 可重入锁的概念:自己可以再次获取自己的内部锁。(如果不可以重入,就会造成死锁)
- 可重入锁也支持在父子环境中(如果方法是同步的,则继承的方法也是同步的?)
出现异常,锁自动释放
- 当一个线程执行的代码出现异常时,其所持有的锁会自动释放
同步具有不可继承性
- 如果父类的方法是同步的,则子类继承后这个方法仍然是同步的
- 但是如果子类自己的新方法中使用了父类的同步方法,则子类的新方法除了使用父类的同步方法那一句是同步的,其余代码还是异步的;如果要使子类的新方法也为同步的,加关键字synchronized在新方法前面即可
synchronized同步语句块
- 用synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的方法,那么B线程则必须等待比较长的时间。在这样的情况下可以使用synchronized同步语句块。
synchronized方法的弊端
synchronized同步代码块的使用
- 当并发线程访问同一个对象object中的synchronized(this)同步代码块时,一段时间内只能有一个线程必须等待当前线程执行完这个代码块后才能执行该代码块。
用同步代码块解决同步方法的弊端
public void doLongTimeTask() {
try {
System.out.println("begin task");
Thread.sleep(3000);
String privateGetData1 = "长时间处理任务后从远程返回的值1 threadName="
+ Thread.currentThread().getName();
String privateGetData2 = "长时间处理任务后从远程返回的值2 threadName="
+ Thread.currentThread().getName();
//同步代码块
synchronized (this) {
getData1 = privateGetData1;
getData2 = privateGetData2;
}
System.out.println(getData1);
System.out.println(getData2);
System.out.println("end task");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
一半异步,一半同步
- 不在synchronized块中就是异步执行,在synchronized中就是同步执行
synchronized代码间的同步性
- 当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”是同一个。
public class ObjectService {
public void serviceMethodA() {
try {
synchronized (this) { //两个同步代码块在不同的方法里边
System.out.println("A begin time=" + System.currentTimeMillis());
Thread.sleep(2000);
System.out.println("A end end=" + System.currentTimeMillis());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void serviceMethodB() {
synchronized (this) { //两个同步代码块在不同的方法里边
System.out.println("B begin time=" + System.currentTimeMillis());
System.out.println("B end end=" + System.currentTimeMillis());
}
}
}
//----------线程A----------
public class ThreadA extends Thread {
private ObjectService service;
public ThreadA(ObjectService service) {
super();
this.service = service;
}
@Override
public void run() {
super.run();
service.serviceMethodA();//方法A
}
}
//---------线程B------
public class ThreadB extends Thread {
private ObjectService service;
public ThreadB(ObjectService service) {
super();
this.service = service;
}
@Override
public void run() {
super.run();
service.serviceMethodB();//方法B
}
}
//---------主函数---------
public class Run {
public static void main(String[] args) {
ObjectService service = new ObjectService();
ThreadA a = new ThreadA(service);
a.setName("a");
a.start();
ThreadB b = new ThreadB(service);
b.setName("b");
b.start();
}
}
验证同步synchronized(this)代码块是锁定当前对象的
将任意对象作为对象监视器
synchronized(非this对象)格式的作用:synchronized(非this对象x)同步代码块
- 在多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)中的代码
- 当持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码块中的代码
锁非this对象具有一定的优点:如果在一个类中有很多个synchronized方法,这时虽然能够实现同步,但会受到阻塞,所以影响运行效率;但如果使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他锁this的同步方法争抢this锁,则可大大提高运行效率。
public class Service {
private String usernameParam;
private String passwordParam;
//String anyString = new String();
//作为类的私有变量时,是共享的,同一个锁
public void setUsernamePassword(String username, String password) {
try {
//作为局部变量,是另一个锁了
String anyString = new String();
synchronized (anyString) {
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "进入同步块");
usernameParam = username;
Thread.sleep(3000);
passwordParam = password;
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "离开同步块");
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
- 可见,使用“synchronized(非this)同步代码块”格式进行同步操作时,对象监视器必须是同一个对象,否则锁的不是同一个对象,结果就是异步,就会交叉运行
细化验证3个结论
- “synchronized(非this对象x)”格式的写法是将x对象本身作为“对象监视器”,这样就可以得到3个结论:
- 当多个线程同时执行synchronized(x){}同步代码块时呈同步效果。
- 当其他线程执行x对象中的synchronized同步方法时呈同步效果
- 当其他线程执行x对象方法里面的synchronized(this)代码时呈现同步效果。
静态同步synchronized方法与synchronized(class)代码块
- 关键字synchronized还可以应用在static静态方法上,如果这样写,那是对当前*.java文件对应的Class类进行持锁
- 都是同步的效果,和将synchronized关键字加到非static方法上使用的效果是一样的。
public class Service {
synchronized public static void printA() {
try {
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "进入printA");
Thread.sleep(3000);
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "退出printA");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized public static void printB() {
System.out.println("线程名称为:" + Thread.currentThread().getName() + "在"
+ System.currentTimeMillis() + "进入printB");
System.out.println("线程名称为:" + Thread.currentThread().getName() + "在"
+ System.currentTimeMillis() + "退出printB");
}
synchronized public void printC() {
System.out.println("线程名称为:" + Thread.currentThread().getName() + "在"
+ System.currentTimeMillis() + "进入printC");
System.out.println("线程名称为:" + Thread.currentThread().getName() + "在"
+ System.currentTimeMillis() + "退出printC");
}
}
//------------A----------
public class ThreadA extends Thread {
@Override
public void run() {
Service.printA();
}
}
//------B---------------
public class ThreadB extends Thread {
@Override
public void run() {
Service.printB();
}
}
//-------------C---------
public class ThreadC extends Thread {
private Service service;
public ThreadC(Service service) {
super();
this.service = service;
}
@Override
public void run() {
service.printC();
}
}
//----------主函数-----
public class Run {s
public static void main(String[] args) {
ThreadA a = new ThreadA();
a.setName("A");
a.start();
ThreadB b = new ThreadB();
b.setName("B");
b.start();
ThreadC c = new ThreadC(service);
c.setName("C");
c.start();
}
}
- 本质上的不同:类锁和对象锁:
- synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁,这是两把不同的锁,所以会异步。
- Class锁可以对类的所有对象实例起作用
- 同步synchronized(class)代码块的作用和synchronized static方法的作用一样,用法如:synchronized(Service.class){ }
数据类型String的常量池特性
- 在JVM中具有String常量池缓存的功能,所以下面代码结果是true:
String a = "a";
String b = "a";
System.out.println(a == b);
- 当两个线程使用的锁都是相同的字符串,可能造成其中一个不能执行。因此大多数情况下,同步synchronized代码块不能使用String作为锁对象
同步synchronized方法无线等待与解决
- 一个线程调用methodA()后就会死循环,导致其他线程不能调用methodB()
public class Service {
synchronized public void methodA() {
System.out.println("methodA begin");
boolean isContinueRun = true;
while (isContinueRun) {
}
System.out.println("methodA end");
}
synchronized public void methodB() {
System.out.println("methodB begin");
System.out.println("methodB end");
}
}
- 可以改为如下:
public class Service {
public void methodA() {
synchronized(object1){ //同步代码块,锁其他对象
System.out.println("methodA begin");
boolean isContinueRun = true;
while (isContinueRun) {
}
System.out.println("methodA end");
}
}
public void methodB() {
synchronized(){ //同步代码块,锁自身对象
System.out.println("methodB begin");
System.out.println("methodB end");
}
}
}
多线程死锁
- 死锁:不同的线程都在等待根本不可能被释放的锁,从而导致所有任务都无法继续完成
public class DealThread implements Runnable {
public String username;
public Object lock1 = new Object();
public Object lock2 = new Object();
public void setFlag(String username) {
this.username = username;
}
@Override
public void run() {
if (username.equals("a")) {
synchronized (lock1) { //获得lock1
try {
System.out.println("username = " + username);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) { //等待lock2
System.out.println("按lock1->lock2代码顺序执行了");
}
}
}
if (username.equals("b")) {
synchronized (lock2) { //获得lock2
try {
System.out.println("username = " + username);
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (lock1) { //等待lock1
System.out.println("按ock2->lock1代码顺序执行了");
}
}
}
}
}
//-----------主函数-----
public class Run {
public static void main(String[] args) {
try {
DealThread t1 = new DealThread();
t1.setFlag("a");
Thread thread1 = new Thread(t1);
thread1.start();
Thread.sleep(100);
t1.setFlag("b");
Thread thread2 = new Thread(t1);
thread2.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
内置类与静态内置类
……
锁对象的改变
- 将任何数据类型作为同步锁时,需要注意的是,是否有多个线程同时持有锁对象,如果同时持有相同的锁对象,则这些线程之间是同步的;如果分别获得锁对象,则这些线程之间就是异步的
public class MyService {
private String lock = "123";
public void testMethod() {
try {
synchronized (lock) { //此时锁对象是“123”
System.out.println(Thread.currentThread().getName() + " begin "
+ System.currentTimeMillis());
lock = "456"; //锁对象改变为“456”
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " end "
+ System.currentTimeMillis());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//-----------主函数------
public class Run1 {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
a.setName("A");
ThreadB b = new ThreadB(service);
b.setName("B");
a.start();
Thread.sleep(50); //此处,等待了50毫秒
b.start(); //所以b取得的锁是 “456”,A、B异步
}
}
- 如果把Thread.sleep(50)注释掉,则a,b抢的是“123”,表现为同步
- 只要对象不变,即使对象的属性被改变,运行的结果还是同步
volatile关键字
- 关键字volatile的主要作用是使变量在多个线程间可见
关键字volatile与死循环
- 如果不是在多继承的情况下,使用继承Thread类和实现Runnale接口在程序运行的结果上并没有太大的区别。如果一旦出现“多继承”的情况,则用实现Runnable接口的方式来处理多线程问题就是很有必要的。
解决同步死循环
public class PrintString implements Runnable {
private boolean isContinuePrint = true;
public boolean isContinuePrint() {
return isContinuePrint;
}
public void setContinuePrint(boolean isContinuePrint) {
this.isContinuePrint = isContinuePrint;
}
public void printStringMethod() {
try {
while (isContinuePrint == true) {
System.out.println("run printStringMethod threadName="
+ Thread.currentThread().getName());
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
printStringMethod();
}
}
//--------------主函数----------
public class Run {
public static void main(String[] args) {
PrintString printStringService = new PrintString();
new Thread(printStringService).start(); //启动线程
System.out.println("我要停止它stopThread="
+ Thread.currentThread().getName());
printStringService.setContinuePrint(false); //设置为false
}
}
- 关键字volatile的作用是强中从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值
volatile private boolean isRunning = true;
- 这样私有堆栈和公共堆栈中的isRunning的值就同步了,以后强制从公共堆栈中取值
- +
解决异步死循环
关键字synchronized和volatile进行比较:
- 关键字volatile是线程同步的轻量级实现,所以volatile的性能肯定比synchronized要好,并且volatile只能修饰变量,而synchronized可以修饰方法,以及代码块。
- 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞
- volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
- 关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性
线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的
volatile非原子性的特性
使用原子类进行i++ 操作
- AtomicInteger
原子类也并不完全安全
synchronized代码块有volatile同步的功能
- synchronized可以使多个线程访问同一个资源具有同步性,而且它还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。