Java多线程05:线程同步
线程同步机制
多个线程访问同一个对象,为了保证数据在方法中被访问时的正确性,就需要线程同步
线程同步就是一种等待机制,多个线程进入这个对象的等待池,形成队列,等待前面的线程执行完毕,下一个线程再使用
在访问时加入锁机制(synchronized),当一个线程获得对象的排他锁就能独占资源,其他线程必须等待,使用后释放锁即可
形成条件:队列+锁
线程不安全案例
银行取钱
public class Main {
public static void main(String[] args) {
Account account = new Account(110);
Drawing me = new Drawing(account, 50, "我");
Drawing you = new Drawing(account, 100, "你");
new Thread(me, "我").start();
new Thread(you, "你").start();
}
}
/**
* 账户类
*/
class Account {
int totalMoney;
public Account(int totalMoney){
this.totalMoney = totalMoney;
}
}
/**
* 取钱类,需要账户名、取钱数、取钱人
*/
class Drawing implements Runnable {
Account account;
int needMoney;
String name;
public Drawing(Account account, int needMoney, String name){
this.account = account;
this.needMoney = needMoney;
this.name = name;
}
@Override
public void run() {
if (account.totalMoney - needMoney < 0){
System.out.println(Thread.currentThread().getName() + ",余额不足");
}
else{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.totalMoney -= needMoney;
System.out.println(Thread.currentThread().getName() + "取走了" + needMoney);
System.out.println("余额还有" + account.totalMoney);
}
}
}
ArrayList集合
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> li = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
li.add(Thread.currentThread().getName());
}).start();
}
/**
* 打印的列表长度没有10000,因为线程同时对一个位置进行操作,覆盖了其他元素
*/
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(li.size());
}
}
线程同步之Synchronized
类似于private关键字保证数据对象只能被方法访问,使用synchronized关键字来实现同步
包括两种用法:synchronized方法和synchronized(obj)块
synchronized方法
该方法控制对“对象“的访问,每个对象对应一把锁,每个方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞
该方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
缺点:将一个大的方法申明为synchronized会影响效率,因为只有需要修改的内容才需要锁,只读的话不应该被锁,此时只能用synchronized(obj)块
public class Main implements Runnable {
int num = 10;
/**
* synchronized同步方法默认锁的对象是this本身
* 如果有多个对象就得用synchronized块指定共同访问的对象
*/
@Override
public synchronized void run() {
while (true) {
if (num <= 0) {
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + num-- + "张票");
}
}
public static void main(String[] args) {
Main main = new Main();
new Thread(main, "老师").start();
new Thread(main, "学生").start();
new Thread(main, "黄牛").start();
}
}
synchronized(obj)块
- obj称为同步监视器,推荐使用共享资源的对象
- 同步方法的监视器就是this对象本身,但是有多个对象操作时,就需要指定一个共有对象,否则每个对象都以自己作为监视器,就失去了同步的意义
public class Main {
public static void main(String[] args) {
Account account = new Account(110);
Drawing me = new Drawing(account, 50, "我");
Drawing you = new Drawing(account, 100, "你");
new Thread(me, "我").start();
new Thread(you, "你").start();
}
}
class Account{
int totalMoney;
public Account(int totalMoney){
this.totalMoney = totalMoney;
}
}
class Drawing implements Runnable{
Account account;
int needMoney;
String name;
public Drawing(Account account, int needMoney, String name){
this.account = account;
this.needMoney = needMoney;
this.name = name;
}
@Override
public void run() {
/**
* 不同对象共同操作的是account对象,因此要锁的是account
*/
synchronized (account) {
if (account.totalMoney - needMoney < 0) {
System.out.println(Thread.currentThread().getName() + ",余额不足");
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.totalMoney -= needMoney;
System.out.println(Thread.currentThread().getName() + "取走了" + needMoney);
System.out.println("余额还有" + account.totalMoney);
}
}
}
}
存在的问题:
一个线程持有锁会导致其他所有需要此锁的线程挂起
加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
低优先级的线程持有锁会导致高优先级的线程一直等待,造成优先级倒置,引起性能问题
若将一个大的方法申明为synchronized方法会影响效率
线程同步之Lock
Lock接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLock类(可重入锁)实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,可以显式加锁、释放锁
import java.util.concurrent.locks.ReentrantLock;
public class Main {
public static void main(String[] args) {
Account account = new Account(100);
Drawing drawing = new Drawing(account, "小红", 50);
Drawing drawing1 = new Drawing(account, "小明", 80);
new Thread(drawing).start();
new Thread(drawing1).start();
}
}
class Account{
int totalMoney;
public Account(int totalMoney){
this.totalMoney = totalMoney;
}
}
class Drawing implements Runnable {
Account account;
String name;
int needMoney;
public Drawing(Account account, String name, int needMoney) {
this.account = account;
this.name = name;
this.needMoney = needMoney;
}
/**
* 创建ReentrantLock对象lock
* 为了确保多个对象调用时是同一个lock,使用static关键字修饰
*/
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
/**
* lock()方法加锁,和try...finally语句一起使用
*/
lock.lock();
try {
if (account.totalMoney - needMoney <= 0) {
System.out.println(name + "取钱,余额不足");
}
else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.totalMoney -= needMoney;
System.out.println(name + "取走了" + needMoney + ",还剩" + account.totalMoney);
}
}
/**
* 在finally语句中放unlock()方法释放锁
*/
finally {
lock.unlock();
}
}
}
synchronized和Lock对比:
Lock是显式锁,需要手动释放;只有代码块锁没有方法锁
Lock锁可以让JVM花费较少的时间来调度线程,性能更好,且具有更好的拓展性(有很多子类)
优先使用顺序:Lock > 同步代码块 > 同步方法
死锁
多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能运行,导致都停止执行的情形
某个同步块同时拥有两个以上对象的锁时,就可能发生”死锁“的问题
产生死锁的四个必要条件:(破除任何一个条件就可以避免死锁发生)
互斥条件:一个资源每次只能被一个进程使用
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:进程已获得的资源,在使用完之前,不能强行剥夺
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
public class Main {
public static void main(String[] args) {
MakeUp makeUp1 = new MakeUp("小红", 0);
MakeUp makeUp2 = new MakeUp("小花", 1);
new Thread(makeUp1).start();
new Thread(makeUp2).start();
}
}
class Mirror{}
class Lip{}
class MakeUp implements Runnable{
String name;
int choice;
public MakeUp(String name, int choice){
this.name = name;
this.choice = choice;
}
/**
* 将镜子和口红属性设置为static,这样可以保证所有对象调用的是同一个镜子和口红
*/
static Mirror mirror = new Mirror();
static Lip lip = new Lip();
@Override
public void run() {
if (choice == 0) {
synchronized (mirror) {
System.out.println(name + "抢到了镜子");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 在同步块里嵌套一个同步块才会死锁,不嵌套就不会死锁
*/
synchronized (lip) {
System.out.println(name + "抢到了口红");
}
}
} else {
synchronized (lip) {
System.out.println(name + "抢到了口红");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (mirror) {
System.out.println(name + "抢到了镜子");
}
}
}
}
}