第一章 JAVA并发编程(基础篇)
1. 线程安全是怎么产生的
JVM内存模型:可见性和原子性。
2. 认识线程安全
定义:当多个线程访问某一个类、对象或方法时,这个类、对象或方法都能表现出与单线程执行时一致的行为,那么这个类、对象或方法就是
线程安全
的。
线程安全问题都是由
全局变量
及静态变量
引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作
,一般都需要考虑线程同步
,否则的话就可能影响线程安全。
class User {
private String name;
private String pass;
public User(String name, String pass) {
this.name = name;
this.pass = pass;
}
public /*synchronized*/ void set(String name, String pass) {
this.name = name;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.pass = pass;
System.out.println(Thread.currentThread().getName() + "-name-" + this.name + "-pass-" + this.pass);
}
}
class UserServlet {
private User user;
public UserServlet() {
user = new User("yk", "...");
}
public void setUser(String name, String pass) {
user.set(name, pass);
}
}
public class DemoThread00 {
public static void main(String[] args) {
UserServlet us = new UserServlet();
new Thread(new Runnable() {
@Override
public void run() {
us.setUser("yk1", "123");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
us.setUser("yk2", "abc");
}
}).start();
}
/*
执行结果:
Thread-0-name-yk2-pass-123
Thread-1-name-yk2-pass-abc
*/
}
程序说明:由于set(String name, String pass)方法没有加锁,以致于原本应该为Thread-0-name-yk1-pass-123的执行结果变成了Thread-0-name-yk2-pass-123。
3. Synchronize方法
Synchronized的作用是
加锁
,所有的synchronized方法都会顺序执行,(这里指占用CPU的顺序)。Synchronized方法执行方式
如下:
- 首先尝试获得锁;
- 如果获得锁,则执行Synchronized的方法体内容;
- 如果无法获得锁则等待,并且不断的尝试去获得锁;
- 一旦锁被释放,则多个线程会同时去尝试获得所,造成锁竞争问题。
锁竞争问题
:在高并发、线程数量高时会引起CPU占用居高不下,或者直接宕机。
class AddCase {
private /*static*/ int count = 0;
public /*synchronized*/ /*static*/ void add() {
count ++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-" + count);
}
}
public class DemoThread02 {
public static void main(String[] args) {
AddCase ac = new AddCase();
AddCase acOne = new AddCase();
new Thread(new Runnable() {
@Override
public void run() {
ac.add();
//AddCase.add();
}
}, "t1").start();
new Thread(new Runnable() {
@Override
public void run() {
ac.add();
//acOne.add();
//AddCase.add();
}
}, "t2").start();
}
}
程序说明:Synchronized作用在非静态方法上代表的
对象锁
,一个对象一个锁,多个对象之间不会发生锁竞争。Synchronized作用在静态方法上则升级为类锁
,所有对象共享一把锁,存在锁竞争。
4. 同步和异步
同步
:必须等待方法执行完毕,才能向下执行,共享资源访问的时候,为了保证线程安全,必须同步。
异步
:不用等待其他方法执行完毕,即可立即执行,例如Ajax异步。
class PrintCase {
public synchronized void printOne() {
System.out.println(Thread.currentThread().getName() + "-hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 注意synchronized注释前后输出结果的变化
public /*synchronized*/ void printTwo() {
System.out.println(Thread.currentThread().getName() + "-hello");
}
}
public class DemoThread03 {
public static void main(String[] args) {
PrintCase pc = new PrintCase();
new Thread(new Runnable() {
@Override
public void run() {
pc.printOne();
}
}, "t1").start();
new Thread(new Runnable() {
@Override
public void run() {
pc.printTwo();
}
}, "t2").start();
}
}
程序说明:对象锁只针对synchronized修饰的方法生效,对象中的所有synchronized方法都会同步执行,而非synchronized方法异步执行。
避免误区
:类中有两个synchronized方法,两个线程分别调用两个方法,相互之间也需要竞争锁,因为两个方法从属于一个对象,而我们是在对象上加锁。
5. 脏读
由于同步和异步方法的执行个性,如果不从全局上进行并发设计很可能会引起数据的不一致,也就是所谓的
脏读
。多个线程访问同一个资源,在一个线程修改数据的过程中,有另外的线程来读取数据,就会引起脏读的产生。
class DirtyReadCase {
private String username = "yk";
private String address = "gp";
public DirtyReadCase() {
}
public synchronized void setVal(String username, String address) {
this.username = username;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.address = address;
System.out.println("setVal结果:" + Thread.currentThread().getName() + "-" + username + "-" + address);
}
public /*synchronized*/ void getVal() {
System.out.println("getVal结果:" + Thread.currentThread().getName() + "-" + username + "-" + address);
}
}
public class DemoThread04 {
public static void main(String[] args) {
DirtyReadCase drc = new DirtyReadCase();
new Thread(new Runnable() {
@Override
public void run() {
drc.setVal("Yk", "Jc");
}
}, "t").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
drc.getVal();
}
/*
getVal结果:main-Yk-gp
setVal结果:t-Yk-Jc
*/
}
程序说明:我们期待的结果原本应该是Yk-Jc,但是由于时间的关系,程序还没有修改完成就已经被读取了,因此出现了错误Yk-gp。
为了避免脏读我们一定要保证数据修改操作的
原子性
、并且对读取操作也要进行同步控制。
6. Synchronized锁重入
同一个线程得到了一个对象的锁之后,再次请求此对象时可以再次获得该对象的锁,即一次性获取两把或多把锁,此过程称之为
锁重入
。
* 同一个对象内的多个synchronized方法可以锁重入。
class RunCase {
public synchronized void runOne() {
runTwo(); // 锁重入,第二把锁释放之后才会继续释放第一把锁。
System.out.println(Thread.currentThread().getName() + "-run1");
}
public synchronized void runTwo() {
System.out.println(Thread.currentThread().getName() + "-run2");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class DemoThread05 {
public static void main(String[] args) {
RunCase rc = new RunCase();
new Thread(new Runnable() {
@Override
public void run() {
rc.runOne();
}
}).start();
}
/*
Thread-0-run2
Thread-0-run1
*/
}
* 父子类可以锁重入。
class ParentIns {
public int i = 10;
public synchronized void runParent() {
i--;
System.out.println("parent-->" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ChildIns extends ParentIns {
public synchronized void runChild() {
while (i > 0) {
i--;
System.out.println("child--->" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
runParent(); // 锁重入
}
}
}
public class DemoThread06 {
public static void main(String[] args) {
ChildIns ci = new ChildIns();
new Thread(new Runnable() {
@Override
public void run() {
ci.runChild();
}
}).start();
}
/*
child--->9
parent-->8
child--->7
parent-->6
child--->5
parent-->4
child--->3
parent-->2
child--->1
parent-->0
*/
}
7. 抛出异常释放锁
一个线程在获得锁之后执行操作,发生错误抛出异常,则自动释放锁。
class ExcCase {
public int i = 0;
public synchronized void setI() {
while (true) {
i++;
System.out.println(Thread.currentThread().getName() + "-run-" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i == 10) {
throw new RuntimeException(); // 抛出异常释放锁
}
}
}
public synchronized void getI() {
System.out.println(Thread.currentThread().getName() + "-run-" + i);
}
}
public class DemoThread07 {
public static void main(String[] args) {
ExcCase ec = new ExcCase();
new Thread(new Runnable() {
@Override
public void run() {
ec.setI();
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
ec.getI();
}
}).start();
}
/*
Thread-0-run-1
Thread-0-run-2
Thread-0-run-3
Thread-0-run-4
Thread-0-run-5
Thread-0-run-6
Thread-0-run-7
Thread-0-run-8
Thread-0-run-9
Thread-0-run-10
Exception in thread "Thread-0" Thread-1-run-10
*/
}
作用:其一,可以利用抛出异常,主动释放锁;其二,程序异常时防止资源被死锁、无法释放;其三,异常释放锁可能导致数据不一致。
8. Synchronized代码块
优势:可以达到
更细粒度
的控制。
class DiffLockCase {
public void runThis() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "-run-对象锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void runClass() {
synchronized (DiffLockCase.class) {
System.out.println(Thread.currentThread().getName() + "-run-类锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private final Object obj = new Object();
public void runObject() {
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "-run-任意锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class DemoThread08 {
public static void main(String[] args) {
test(1);
/*
DiffLockCase diffLockCase1 = new DiffLockCase();
DiffLockCase diffLockCase2 = new DiffLockCase();
new Thread(new Runnable() {
@Override
public void run() {
diffLockCase1.runThis();
}
}).start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
diffLockCase2.runClass();
}
}).start();*/
}
public static void test(int type) {
final DiffLockCase dlcOne = new DiffLockCase();
final DiffLockCase dlcTwo = new DiffLockCase();
if (type == 1) {
System.out.println("对象锁测试");
} else if (type == 2) {
System.out.println("类锁测试");
} else {
System.out.println("任意锁测试");
}
new Thread(new Runnable() {
@Override
public void run() {
if (type == 1) {
dlcOne.runThis();
} else if (type == 2) {
dlcOne.runClass();
} else {
dlcOne.runObject();
}
}
}, "t1").start();
new Thread(new Runnable() {
@Override
public void run() {
if (type == 1) {
dlcTwo.runThis();
} else {
if (type == 2) {
dlcTwo.runClass();
} else {
dlcTwo.runObject();
}
}
}
}, "t2").start();
}
}
问:同种类型的锁之间互斥?
答:同种this锁和Object锁之间并没发生互斥,只有同种Class锁才发生了互斥。下一节中代码注释会有说明。
Synchronized代码块使用注意事项:其一,同一对象(或者不同对象)的不同类型锁(对象锁、类锁、任意锁)之间互不干扰;其二,同类型锁之间不一定互斥。同一对象相同类型锁互斥,而不同对象相同类型锁不一定互斥。
9. 对象锁引用
不要在线程中修改对象锁的引用,引用被改变会导致锁失效。线程A修改了对象锁的引用,则线程B实际得到了新的对象锁,而不是锁被释放了,因此引发了线程安全问题。
class ChangeLockCase {
// 教程上说:不要在线程中修改对象锁的引用,引用被修改会导致锁失效。但“此处”未证实,欢迎大家指正。
private String lock = "lock handler";
// 当同一个类的多个不同对象(实例)调用 加this锁和object锁 的方法时,多个对象之间不会发生锁竞争。
// 但是当调用 加class锁或者其他对象锁 的方法时,多个对象之间会发生锁竞争。
private final Object object = new Object();
public void method() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "-start");
//说明:修改锁引用之后,锁竞争依然存在。真让人头大!
//lock = "change lock handler";
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-end");
}
}
}
public class DemoThread10 {
public static void main(String[] args) {
ChangeLockCase clcOne = new ChangeLockCase();
ChangeLockCase clcTwo = new ChangeLockCase();
new Thread(new Runnable() {
@Override
public void run() {
clcOne.method();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
clcTwo.method();
}
}).start();
}
}
在线程中修改了锁对象的属性,而不修改引用则不会引起锁失效、不会产生线程安全问题。
class Person {
private String name;
private int age;
public Person() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class DemoThread10 {
private Person per = new Person();
public void changeUser(String name, int age) {
// 其他对象锁
synchronized (per) {
System.out.println("Start:" + Thread.currentThread().getName() + "-" + per.getName() + "-" + per.getAge());
//修改锁对象的引用,引发线程安全问题,“此处”得到证实。
//per = new Person();
//只修改锁对象的属性,不会引发线程安全问题
per.setName(name);
per.setAge(age);
System.out.println("Modified:" + Thread.currentThread().getName() + "-" + per.getName() + "-" + per.getAge());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("End:" + Thread.currentThread().getName() + "-" + per.getName() + "-" + per.getAge());
}
}
public static void main(String[] args) {
DemoThread10 m = new DemoThread10();
new Thread(new Runnable() {
@Override
public void run() {
m.changeUser("yk", 99);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
m.changeUser("yk-1", 100);
}
}).start();
}
}
10. 并发与死锁
两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
class DeadLockCase {
private final Object lockOne = new Object();
private final Object lockTwo = new Object();
public void executeOne() {
synchronized (lockOne) {
System.out.println(Thread.currentThread().getName() + "-lockOne-executeOne");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockTwo) {
System.out.println(Thread.currentThread().getName() + "-lockTwo-executeOne");
}
}
}
public void executeTwo() {
synchronized (lockTwo) {
System.out.println(Thread.currentThread().getName() + "-lockTwo-executeTwo");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockOne) {
System.out.println(Thread.currentThread().getName() + "-lockOne-executeTwo");
}
}
}
}
public class DemoThread12 {
public static void main(String[] args) {
DeadLockCase dlc = new DeadLockCase();
new Thread(new Runnable() {
@Override
public void run() {
dlc.executeOne();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
dlc.executeTwo();
}
}).start();
}
}
11. 线程间通讯
每个线程都是独立运行的个体,线程通讯能让多个线程之间协同工作。
class NotWaitNotifyCase {
private volatile List<String> list = new ArrayList<>();
private volatile boolean canGet = false;
public void set() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-" + i);
list.add("A");
if (i == 5) {
canGet = true;
System.out.println(Thread.currentThread().getName() + "发出通知");
}
}
}
public void get() {
while (true) {
if (canGet) {
for (String l : list) {
System.out.println(l);
}
break;
}
}
}
}
class WaitNotifyCase {
private List<String> list = new ArrayList<>();
private final Object obj = new Object();
public void set() {
synchronized (obj) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "-" + i);
list.add("A");
if (list.size() == 5) {
//注意区别:notify()触发后不释放锁
obj.notify();
System.out.println(Thread.currentThread().getName() + "发出通知");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public void get() {
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "发起等待");
try {
//注意区别:wait()触发后释放锁
obj.wait();
System.out.println(Thread.currentThread().getName() + "被动唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
for (String l : list) {
System.out.println(l);
}
}
}
}
public class DemoThread17 {
public static void main(String[] args) {
NotWaitNotifyCase nwnc = new NotWaitNotifyCase();
//WaitNotifyCase wnc = new WaitNotifyCase();
new Thread(new Runnable() {
@Override
public void run() {
nwnc.get();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
nwnc.set();
}
}).start();
}
}
程序说明:其一,wait/notify必须与synchronized一同使用;其二,wait释放锁、notify不释放锁。
Notify只会通知一个wait中的线程,并把锁给他,不会产生锁竞争问题,但是该线程处理完毕之后必须再次notify或notifyAll,完成类似链式的操作。NotifyAll会通知所有wait中的线程,会产生锁竞争问题。
class NotifyAllCase {
public synchronized void runOne() {
System.out.println("进入runOne方法");
//this.notify();
this.notifyAll();
System.out.println("runOne执行完毕");
}
public synchronized void runTwo() {
System.out.println("进入runTwo方法");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// this.notify();
// System.out.println("runTwo发起通知");
System.out.println("runTwo执行完毕");
}
public synchronized void runThree() {
System.out.println("进入runThree方法");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("runThree执行完毕");
}
}
public class DemoThread22 {
public static void main(String[] args) {
NotifyAllCase nac = new NotifyAllCase();
new Thread(new Runnable() {
@Override
public void run() {
nac.runTwo();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
nac.runThree();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
nac.runOne();
}
}).start();
}
}
12. 案例实战
使用synchronized、wait、notify实现带阻塞的线程安全队列。
class MQueue {
private List<String> list = new ArrayList<>();
private int maxSize;
private final Object lock = new Object();
public MQueue(int maxSize) {
this.maxSize = maxSize;
System.out.println("阻塞队列的最大长度为" + maxSize);
}
public void put(String element) {
synchronized (lock) {
if (list.size() == maxSize) {
System.out.println(Thread.currentThread().getName() + "-队列已满");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(element);
System.out.println(Thread.currentThread().getName() + "-队列中加入了" + element);
lock.notifyAll(); // notifyAll()执行完毕后会释放锁吗?此处保留,欢迎读者批评指正。
//System.out.println(Thread.currentThread().getName() + "-发起通知");
}
}
public String take() {
synchronized (lock) {
if (list.size() == 0) {
System.out.println(Thread.currentThread().getName() + "-队列为空");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String s = list.remove(0);
System.out.println(Thread.currentThread().getName() + "-队列中取出了" + s);
lock.notifyAll();
//System.out.println(Thread.currentThread().getName() + "-发起通知");
return s;
}
}
}
public class DemoThread20 {
public static void main(String[] args) {
MQueue mQueue = new MQueue(5);
new Thread(new Runnable() {
@Override
public void run() {
mQueue.put("1");
mQueue.put("2");
mQueue.put("3");
mQueue.put("4");
mQueue.put("5");
mQueue.put("6");
mQueue.put("7");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
mQueue.put("21");
mQueue.put("22");
mQueue.put("23");
mQueue.put("24");
mQueue.put("25");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
mQueue.take();
mQueue.take();
mQueue.take();
mQueue.take();
mQueue.take();
mQueue.take();
mQueue.take();
mQueue.take();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
mQueue.take();
mQueue.take();
mQueue.take();
mQueue.take();
mQueue.take();
}
}).start();
}
}