JavaSE学习笔记25:多线程(三)
多线程(三)
线程安全
什么时候数据在多线程并发的环境下会存在安全问题
- 多线程并发
- 有共享数据
- 共享数据有修改行为
满足以上3个条件之后,就会存在线程安全问题
同步机制
如何解决?线程排队执行(不能并发),专业术语叫做“线程同步机制”
用排队执行解决线程安全问题,这种机制被称为:线程同步机制。
注意:线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位。只有数据安全了,我们才可以谈效率,数据不安全,没有效率的事。
说到线程同步这块,涉及到两个专业术语:
-
异步编程模型:
线程T1和线程T2,各自执行各自的,T1不管T2,T2不管T1,谁也不需要等谁,其实就是多线程并发(效率较高)
-
同步编程模型:
线程T1和T2,在线程T1执行的时候,必须等待T2线程执行结束,或者说在T2线程执行的时候,必须等待T1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。(效率较低,线程排队执行)
异步就是并发,同步就是排队!!!
synchronized的写法:
- 同步代码块
synchronized(线程共享对象){
同步代码块;
}
-
在实例方法上使用synchronized。表示共享对象一定是this,并且同步代码块是整个方法体。
-
在静态方法上使用synchronized。表示找类锁,类锁永远只有1把。
对象锁:1个对象1把锁,100个对象100把锁。
类锁:100个对象,也可能只是1把类锁
例子1
package se5.threadsafe;
public class Test {
public static void main(String[] args) {
//创建账户对象(只创建一个)
Account act = new Account("act-001",10000);
//创建两个线程
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
//设置name
t1.setName("t1");
t2.setName("t2");
//启动线程取款
t1.start();
t2.start();
/**
* 结果:
* t1对act-001取款5000.0成功,余额5000.0
* t2对act-001取款5000.0成功,余额5000.0
*/
}
}
package se5.threadsafe;
/*
银行账户
不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题
*/
public class Account {
//账号
private String actno;
//余额
private double balance;
public Account() {}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款方法
public void withdraw(double money){
//t1和t2并发这个方法。。。(t1和t2是两个栈,两个栈操作堆中同一个对象)
//取款之前的余额
double before = this.getBalance();
//取款之后的余额
double after = before - money;
//在这里模拟一下网络延迟,100%出问题
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
//思考:t1执行到这里了,但还没有执行这行代码,t2线程进来withdraw方法了,此时一定出问题。
this.setBalance(after);
}
}
package se5.threadsafe;
public class AccountThread extends Thread {
//两个线程必须共享同一个账户对象
private Account act;
//通过构造方法传递过来账户对象
public AccountThread(Account act){
this.act = act;
}
public void run(){
//run方法的执行表示取款操作
//假设取款5000
double money = 5000;
//取款
//多线程并发执行这个方法
act.withdraw(money);
System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款" + money + "成功,余额" + act.getBalance());
}
}
修改后
package se5.threadsafe2;
public class Test {
public static void main(String[] args) {
//创建账户对象(只创建一个)
Account act = new Account("act-001",10000);
//创建两个线程
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
//设置name
t1.setName("t1");
t2.setName("t2");
//启动线程取款
t1.start();
t2.start();
/**
* 结果:
* t1对act-001取款5000.0成功,余额5000.0
* t2对act-001取款5000.0成功,余额0.0
*/
}
}
package se5.threadsafe2;
/*
银行账户
使用线程同步机制,解决线程安全问题
*/
public class Account {
//账号
private String actno;
//余额
private double balance;
public Account() {}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款方法
public void withdraw(double money){
//以下这几行代码必须是线程排队的,不能并发
//一个线程把这里的代码全部执行结束之后,另一个线程才能进来
/**
* 线程同步机制的语法是:
* synchronized(){
* //线程同步代码块
* }
* synchronized后面小括号中传的数据是相当关键的,这个数据必须是多线程共享的数据,才能达到多线程排队。
* ()写什么?
* 那要看你想让哪些线程同步。
* 假设t1、t2、t3、t4、t5有5线程
* 只希望t1、t2、t3排队,t4、t5不需要排队
* 一定要在小括号中写一个t1、t2、t3共享的对象,而这个对象对于t4、t5来说不是共享的。
*
* 这里的共享对象是:账户对象
* 账户对象是共享的,那么this就是账户对象!!!
* 不一定是this,这里只要是多线程共享的对象就行。
*
* 以下代码执行原理:
* 1.假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
* 2.假设t1先执行了,遇到了synchronized,这个时候自动找后面共享对象的对象锁,找到之后,
*并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代
*码块代码结束,这把锁才会释放。
* 3.假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的
*这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束了,
*t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序。
*
* 这样就达到了线程排队执行。
* 这里需要注意:共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象锁共享的。
*/
synchronized (this){
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
}
package se5.threadsafe2;
public class AccountThread extends Thread {
//两个线程必须共享同一个账户对象
private Account act;
//通过构造方法传递过来账户对象
public AccountThread(Account act){
this.act = act;
}
public void run(){
//run方法的执行表示取款操作
//假设取款5000
double money = 5000;
//取款
//多线程并发执行这个方法
act.withdraw(money);
System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款" + money + "成功,余额" + act.getBalance());
}
}
例子2
package com.thread2;
//不安全的买票
//线程不安全,有负数
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
new Thread(station,"黄牛张三").start();
new Thread(station,"黄牛李四").start();
new Thread(station,"黄牛王五").start();
/**
* ...
* 黄牛王五拿到3
* 黄牛李四拿到2
* 黄牛张三拿到1
* 黄牛王五拿到-1
* 黄牛李四拿到0
*/
}
}
class BuyTicket implements Runnable{
//票
private int ticketNums = 10;
boolean flag = true;//外部停止方式
@Override
public void run() {
//买票
while (flag){
buy();
}
}
private void buy(){
//判断是否有票
if (ticketNums <= 0){
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//买票
System.out.println(Thread.currentThread().getName()+ "拿到" + ticketNums--);
}
}
使用synchronized修改后
package com.thread2;
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
new Thread(station,"黄牛张三").start();
new Thread(station,"黄牛李四").start();
new Thread(station,"黄牛王五").start();
/**
* 黄牛张三拿到10
* 黄牛张三拿到9
* 黄牛张三拿到8
* 黄牛李四拿到7
* 黄牛李四拿到6
* 黄牛李四拿到5
* 黄牛王五拿到4
* 黄牛李四拿到3
* 黄牛李四拿到2
* 黄牛李四拿到1
*/
}
}
class BuyTicket implements Runnable{
//票
private int ticketNums = 10;
boolean flag = true;//外部停止方式
@Override
public void run() {
//买票
while (flag){
buy();
}
}
//synchronized 同步方法,锁的是this
private synchronized void buy(){
//判断是否有票
if (ticketNums <= 0){
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//买票
System.out.println(Thread.currentThread().getName()+ "拿到" + ticketNums--);
}
}
死锁
死锁不会出现异常,也不会出现错误,程序一直僵持在那里。这种错误最难调试。
package com.thread2;
//死锁:多个线程互相抱着对方需要的资源,然后形成僵持
public class DeadLock {
public static void main(String[] args) {
Makeup g1 = new Makeup(0,"女孩1");
Makeup g2 = new Makeup(1,"女孩2");
g1.start();
g2.start();
}
}
//口红
class Lipstick{}
//镜子
class Mirror{}
class Makeup extends Thread {
//需要的资源只有一份,用static来保证只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;//选择
String girlName;//使用化妆品的人
Makeup(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
//化妆
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//化妆,互相持有对方的锁,就是需要拿到对方的资源
private void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) {//获得口红的锁
System.out.println(this.girlName + "获得口红的锁");
Thread.sleep(1000);
synchronized (mirror) {//一秒后想获得镜子
System.out.println(this.girlName + "获得镜子的锁");
}
}
} else {
synchronized (mirror) {//获得镜子的锁
System.out.println(this.girlName + "获得镜子的锁");
Thread.sleep(2000);
synchronized (lipstick) {//两秒后想获得口红
System.out.println(this.girlName + "获得口红的锁");
}
}
}
}
}
上面这个程序执行结果会一直僵持,停不下来。
Lock锁
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。
class A{
private final ReentrantLock lock = new ReentrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码
}
finally{
lock.unlock();
//如果同步代码有异常,要将unlock()写入finally语句块
}
}
}
package com.thread2;
import java.util.concurrent.locks.ReentrantLock;
//测试Lock锁
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
class TestLock2 implements Runnable {
int ticketNums = 10;
//定义Lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
lock.lock();//加锁
if (ticketNums > 0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
}else {
break;
}
}finally {
//解锁
lock.unlock();
}
}
}
}
synchronized与Lock的对比
- Lock是显示锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,处理作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
关于wait和notify方法
-
wait和notify方法不是线程对象的方法,是java任何一个java对象都有的方法,因为这两个方法是Object类中自带的。
wait方法和notify方法不是通过线程对象调用的
-
wait()方法的作用
Object o = new Object(); o.wait(); /* 表示:让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。 o.wait();方法的调用,会让当前线程(正在o对象上活动的线程)进入等待状态, wait(long timeout):指定等待的毫秒数 */ -
notify()方法的作用
Object o = new Object(); o.notify(); /* 表示:唤醒正在o对象上等待的线程 还有一个notifyAll()方法:这个方法是唤醒o对象上处于等待的所有线程 */
生产者和消费者模式
-
使用wait方法和notify方法实现生产者和消费者模式
-
什么是生产者和消费者模式?
生产线程负责生产,消费线程负责消费
生产线程和消费线程要达到均衡
这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法 -
wait和notify方法不是线程对象的方法,是普通java对象都有的方法
-
wait方法和notify方法建立在线程同步的基础之上,因为多线程要同时操作一个仓库,有线程安全问题
-
wait方法的作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且在释放掉t线程之前占有o对象的锁
-
notify方法的作用:o.notify()让正在o对象上等待的线程被唤醒,只是通知,不会释放o对象上之前占有的锁
例子:
模拟:仓库采用list集合,list集合中假设只能存储1个元素,1个元素就表示仓库满了,如果list集合中元素个数是0,就表示仓库空了。保证list集合中永远都是最多存储1个元素,必须做到这种效果:生产1个消费1个。
package se5.thread;
import java.util.ArrayList;
import java.util.List;
public class ThreadTest16 {
public static void main(String[] args) {
//创建一个仓库对象,共享的
List list = new ArrayList();
//创建两个线程对象
//生产者线程
Thread t1 = new Thread(new Producer(list));
//消费者线程
Thread t2 = new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
//生产线程
class Producer implements Runnable{
//仓库
private List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
//一直生产(使用死循环模拟)
while (true){
//给仓库对象list加锁
synchronized (list){
if (list.size() > 0){//大于0,说明仓库中已经有1个元素了
//当前线程进入等待状态,并且释放Producer之前占有的list集合的锁
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能执行到这里说明仓库是空的,可以生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
//唤醒消费者进行消费
//list.notify();
list.notifyAll();
}
}
}
}
//消费线程
class Consumer implements Runnable{
//仓库
private List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
//一直消费
while (true){
synchronized (list){
if (list.size() == 0){
try {
//仓库空了
//消费者线程等待,释放掉list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能执行到此处说明仓库中有数据,进行消费
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
//唤醒生产者进行生产
//list.notify();
list.notifyAll();
}
}
}
}

浙公网安备 33010602011771号