一. 什么是线程同步
处理多线程问题时,当多个线程访问同一对象,并且某些线程还想修改这个对象时,就需要线程同步。
线程同步其实是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕,下一个线程再使用。
线程同步的条件:等待队列 和 锁
如果不进行线程同步,处理多线程问题时会出现线程不安全的情况:
public class TestThread {
public static void main(String[] args) {
Test test = new Test();
new Thread(test,"张三").start();
new Thread(test,"李四").start();
new Thread(test,"王五").start();
}
}
class Test implements Runnable{
private int ticketNum = 10;
@Override
public void run() {
while (ticketNum > 0) {
buy();
try {
Thread.sleep(100); // 模拟延时,放大问题
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 买票方法
public void buy(){
if (ticketNum<=0)
return;
System.out.println(Thread.currentThread().getName() + "抢到了第" + (ticketNum--) + "张票");
}
}
结果:
李四抢到了第10张票
王五抢到了第8张票
张三抢到了第9张票
李四抢到了第7张票
张三抢到了第7张票
王五抢到了第6张票
李四抢到了第5张票
王五抢到了第4张票
张三抢到了第5张票
李四抢到了第3张票
张三抢到了第3张票
王五抢到了第3张票
张三抢到了第2张票
李四抢到了第1张票
二. 实现线程同步
由于同一进程的多个线程共享同一块存储空间,带来方便的同时也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized),当一个线程获得对象的排它锁,独占资源,其他访问线程必须等待,使用后释放锁。
锁机制(synchronized)包括两种用法:synchronized方法 和 synchronized块。
① 同步方法(synchronized方法):
由于我们可以通过private关键字来保证数据对象只能通过方法访问,所以我们可以针对方法进行同步。
同步方法只需在方法前面加上synchronized关键字:public synchronized void method(){}
synchronized方法控制对对象的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程堵塞,方法一旦执行,就独占该锁,直到方法执行完毕释放锁。
public class TestThread2 {
public static void main(String[] args) {
Test test = new Test();
new Thread(test,"张三").start();
new Thread(test,"李四").start();
new Thread(test,"王五").start();
}
}
class Test implements Runnable{
private int ticketNum = 10;
@Override
public void run() {
while (ticketNum > 0) {
buy();
try {
Thread.sleep(100); // 模拟延时,放大问题
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 买票方法
public synchronized void buy(){ //加上synchronized关键字变为同步方法
if (ticketNum<=0)
return;
System.out.println(Thread.currentThread().getName() + "抢到了第" + (ticketNum--) + "张票");
}
}
结果:
张三抢到了第10张票
王五抢到了第9张票
李四抢到了第8张票
张三抢到了第7张票
王五抢到了第6张票
李四抢到了第5张票
王五抢到了第4张票
张三抢到了第3张票
李四抢到了第2张票
李四抢到了第1张票
② 同步块(synchronized块):
synchronized(Obj){}
Obj 称之为 同步监视器,Obj可以是任何对象,但是推荐使用共享资源作为同步监视器。
同步方法中无需指定同步监视器,因为其同步监视器就是This,即对象本身,或者是class。
模拟银行取钱,如果不进行同步则会出现线程不安全:
public class TestWithDraw {
public static void main(String[] args) {
Account account = new Account(200);
Drawing first = new Drawing(account,150);
Drawing second = new Drawing(account,100);
first.start();
second.start();
}
}
class Account {
int money; // 余额
public Account(int money) {
this.money = money;
}
}
class Drawing extends Thread{
private Account account;
private int drawingMoney; // 要取的金额
public Drawing(Account account, int drawingMoney) {
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
if (account.money-drawingMoney<0){
System.out.println(Thread.currentThread().getName()+"钱不够了,取不了");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money -= drawingMoney;
System.out.println("账户余额为:"+account.money);
}
}
结果:
账户余额为:-50
账户余额为:-50
这里如果用同步方法进行线程同步:
public class TestWithDraw {
public static void main(String[] args) {
Account account = new Account(200);
Drawing first = new Drawing(account,150);
Drawing second = new Drawing(account,100);
first.start();
second.start();
}
}
class Account {
int money; // 余额
public Account(int money) {
this.money = money;
}
}
class Drawing extends Thread{
private Account account;
private int drawingMoney; // 要取的金额
public Drawing(Account account, int drawingMoney) {
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public synchronized void run() { // 同步方法
if (account.money-drawingMoney<0){
System.out.println(Thread.currentThread().getName()+"钱不够了,取不了");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money -= drawingMoney;
System.out.println("账户余额为:"+account.money);
}
}
结果还是不安全:
账户余额为:-50
账户余额为:-50
因为同步方法的同步监视器锁定的是this(当前对象本身),本例中锁定的是Drawing对象,而共享资源是Account对象,应锁定account,所以应采用同步块(synchronized块)进行线程同步:
public class TestWithDraw {
public static void main(String[] args) {
Account account = new Account(200);
Drawing first = new Drawing(account,150);
Drawing second = new Drawing(account,100);
first.start();
second.start();
}
}
class Account {
int money; // 余额
public Account(int money) {
this.money = money;
}
}
class Drawing extends Thread{
private Account account;
private int drawingMoney; // 要取的金额
public Drawing(Account account, int drawingMoney) {
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
synchronized (account){ // 同步块
if (account.money-drawingMoney<0){
System.out.println(Thread.currentThread().getName()+"钱不够了,取不了");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money -= drawingMoney;
System.out.println("账户余额为:"+account.money);
}
}
}
正确结果:
账户余额为:50
Thread-1钱不够了,取不了
三. 死锁
多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。某一同步块同时拥有两个以上对象的锁就可能发生“死锁”的问题。
模拟死锁:
public class TestWithDraw {
public static void main(String[] args) {
Makeup g1 = new Makeup(0,"灰公主");
Makeup g2 = new Makeup(1,"白雪公主");
// g1 获得镜子等待口红,g2 获得口红等待镜子,两者相互等待导致死锁
g1.start();
g2.start();
}
}
// 口红
class Lipstick{
}
// 镜子
class Mirror{
}
// 化妆
class Makeup extends Thread{
// 口红、镜子资源只有一份,用static来保证只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
private int choice;
private String name;
public Makeup(int choice, String name) {
this.choice = choice;
this.name = name;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void makeup() throws InterruptedException {
if (choice==0){
synchronized (lipstick){ // 获得口红的锁
System.out.println(this.name+"获得了口红");
Thread.sleep(1000);
synchronized (mirror){ // 获得镜子的锁
System.out.println(this.name+"获得了镜子");
}
}
}else{
synchronized (mirror){ // 获得镜子的锁
System.out.println(this.name+"获得了镜子");
Thread.sleep(1000);
synchronized (lipstick){ // 获得口红的锁
System.out.println(this.name+"获得了口红");
}
}
}
}
}
结果:程序不会结束,一直在运行
产生死锁的四个必要条件:
① 互斥条件:一个资源每次只能被一个进程使用
② 请求与保持条件:一个进程因请求资源而堵塞时,对已获得的资源保持不释放
③ 不剥夺条件:进程获得的资源在未使用完之前,不能强行剥夺
④ 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
避免死锁:只要破坏发生死锁的一个或多个条件就能避免死锁发生
四. 使用Lock锁实现线程同步
从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问,那就是Lock。
Lock是一个接口,ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示地加锁、解锁。
public class Test {
public static void main(String[] args) {
TLock tLock = new TLock();
new Thread(tLock).start();
new Thread(tLock).start();
new Thread(tLock).start();
}
}
class TLock implements Runnable{
private Lock lock = new ReentrantLock(); // 定义Lock锁
private int ticketNum = 10;
@Override
public void run() {
while (true) {
lock.lock(); // 加锁
try{
if (ticketNum>0){
System.out.println(ticketNum--);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else
break;
}finally {
lock.unlock(); // 解锁
}
}
}
}
结果:
10
9
8
7
6
5
4
3
2
1
参考资料:
Java并发编程:Lock https://www.cnblogs.com/dolphin0520/p/3923167.html.
Java并发编程:synchronized https://www.cnblogs.com/dolphin0520/p/3923737.html