基本概念
- 当多个线程同时访问同一种共享资源时,可能会造成数据的覆盖等不一致性问题,此时就需要对线程之间进行通信和协调,该机制就叫做线程的同步机制。
- 多个线程并发读写同一个临界资源时会发生线程并发安全问题。
- 异步操作:多线程并发的操作,各自独立运行。
- 同步操作:多线程串行的操作,先后执行的顺序。 ```java package com.lagou.task18;
/**
- @author lijing
- @date 2020/10/15 15:05
@description */ public class AccountRunnableTest implements Runnable{ private int balance; public AccountRunnableTest() { }
public AccountRunnableTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "已启动");
///1.模拟从后台查询账户余额的过程
int temp=getBalance();
//2.模拟取款200元的过程
if(temp>=200){
System.out.println("正在出钞中,请骚等..");
temp -=200;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走你的钞票");
}else{
System.out.println("余额不足,请核对的账户余额");
}
//3.模拟将最新的账户余额写入到后台
setBalance(temp);
}
public static void main(String[] args){
AccountRunnableTest account=new AccountRunnableTest(1000);
Thread t1=new Thread(account);
Thread t2=new Thread(account);
t1.start();
t2.start();
System.out.println("主线程开始等待...");
try {
t1.join();
// t2.start();//也就是等待线程一取款操作结束后再启动线程二—串行 没有多线程
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为"+account.getBalance());
} }
<a name="E9JcA"></a>
# 解决方案
由程序结果可知:当两个线程同时对同一个账户进行取款时,导致最终的账户余额不合理。<br />引发原因:线程一执行取款时还没来得及将取款后的余额写入后台,线程二就已经开始取款。<br />解决方案:让线程一执行完毕取款操作后,再让线程二执行即可,将线程的并发操作改为串行操作。<br />经验分享:在以后的开发尽量减少串行操作的范围,从而提高效率。
<a name="WenYy"></a>
# 实现方式
在Java语言中使用synchronized关键字来实现同步/对象锁机制从而保证线程执行的原子性,具体方式如下:<br />使用同步代码块的方式实现部分代码的锁定,格式如下:<br />synchronized(类类型的引用) {<br />编写所有需要锁定的代码;<br />}<br />使用同步方法的方式实现所有代码的锁定。直接使用synchronized关键字来修饰整个方法即可<br />该方式等价于:<br />synchronized(this) { 整个方法体的代码 }
```java
package com.lagou.task18;
/**
* @author lijing
* @date 2020/10/15 15:05
* @description
*/
public class AccountRunnableTest implements Runnable{
private int balance;
private Demo dm=new Demo();
public AccountRunnableTest() {
}
public AccountRunnableTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "已启动");
synchronized (dm) {
//synchronized(new Demo()){ 锁不住 需要同一个锁
///1.模拟从后台查询账户余额的过程
int temp=getBalance();
//2.模拟取款200元的过程
if(temp>=200){
System.out.println("正在出钞中,请骚等..");
temp -=200;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走你的钞票");
}else{
System.out.println("余额不足,请核对的账户余额");
}
//3.模拟将最新的账户余额写入到后台
setBalance(temp);
}
}
public static void main(String[] args){
AccountRunnableTest account=new AccountRunnableTest(1000);
Thread t1=new Thread(account);
Thread t2=new Thread(account);
t1.start();
t2.start();
System.out.println("主线程开始等待...");
try {
t1.join();
// t2.start();//也就是等待线程一取款操作结束后再启动线程二--串行 没有多线程
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为"+account.getBalance());
}
}
class Demo{}
静态方法的锁定
- 当我们对一个静态方法加锁,如:
- public synchronized static void xxx(){….}
- 那么该方法锁的对象是类对象。每个类都有唯一的一个类对象。获取类对象的方式:类名.class。
- 静态方法与非静态方法同时使用了synchronized后它们之间是非互斥关系的。
- 原因在于:静态方法锁的是类对象而非静态方法锁的是当前方法所属对象。 ```java package com.lagou.task18;
/**
- @author lijing
- @date 2020/10/15 15:26
@description */ public class AccountThreadTest extends Thread{ private int balance; //类属于类层级 private static Demo dm=new Demo();
public AccountThreadTest() { }
public AccountThreadTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override public /synchronized/ void run() {
System.out.println("线程" + Thread.currentThread().getName() + "已启动");
synchronized (dm) { //每一个对象都有一个成员变量dm 所以锁不住 如果要锁住需要static关键字修饰dm
///1.模拟从后台查询账户余额的过程
int temp=getBalance();
//2.模拟取款200元的过程
if(temp>=200){
System.out.println("正在出钞中,请骚等..");
temp -=200;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走你的钞票");
}else{
System.out.println("余额不足,请核对的账户余额");
}
//3.模拟将最新的账户余额写入到后台
setBalance(temp);
}
}
public static void main(String[] args) {
AccountThreadTest att1=new AccountThreadTest(1000);
att1.start();
AccountThreadTest att2=new AccountThreadTest(1000);
att2.start();
System.out.println("主线程开始等待...");
try {
att1.join();
// t2.start();//也就是等待线程一取款操作结束后再启动线程二—串行 没有多线程
att2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为"+att1.getBalance());
} }
<a name="g4gbx"></a>
# 注意事项
- 使用synchronized保证线程同步应当注意:
- 多个需要同步的线程在访问同步块时,看到的应该是同一个锁对象引用。
- 在使用同步块时应当尽量减少同步范围以提高并发的执行效率。
<a name="a0oS8"></a>
# 线程安全类和不安全类
- StringBuffer类是线程安全的类,但StringBuilder类不是线程安全的类。
- Vector类和 Hashtable类是线程安全的类,但ArrayList类和HashMap类不是线程安全的类。
- Collections.synchronizedList() 和 Collections.synchronizedMap()等方法实现安全。
<a name="IMOTQ"></a>
# 死锁的概念
线程一执行的代码:<br />public void run(){<br />synchronized(a){ //持有对象锁a,等待对象锁b<br />synchronized(b){<br />编写锁定的代码;<br />}<br />}<br />}<br />线程二执行的代码:<br />public void run(){<br />synchronized(b){ //持有对象锁b,等待对象锁a<br />synchronized(a){<br />编写锁定的代码;<br />}<br />}<br />}<br />注意:<br />在以后的开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用!
<a name="58TVb"></a>
# 使用Lock(锁)实现线程同步
<a name="IoLdy"></a>
## 基本概念
从Java5开始提供了更强大的线程同步机制—使用显式定义的同步锁对象来实现。<br />java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。<br />该接口的主要实现类是ReentrantLock类,该类拥有与synchronized相同的并发性,在以后的线程安全控制中,经常使用ReentrantLock类显式加锁和释放锁。
<a name="AezvV"></a>
## 常用的方法
| ReentrantLock() | 使用无参方式构造对象 |
| --- | --- |
| void lock() | 获取锁 |
| void unlock() | 释放锁 |
<a name="UvkJ0"></a>
## 与synchronized方式的比较
- Lock是显式锁,需要手动实现开启和关闭操作,而synchronized是隐式锁,执行锁定代码后自动释放。
- Lock只有同步代码块方式的锁,而synchronized有同步代码块方式和同步方法两种锁。
- 使用Lock锁方式时,Java虚拟机将花费较少的时间来调度线程,因此性能更好。
<a name="1lL0a"></a>
## Object类常用的方法
| void wait() | 用于使得线程进入等待状态,直到其它线程调用notify()或notifyAll()方法 |
| --- | --- |
| void wait(long timeout) | 用于进入等待状态,直到其它线程调用方法或参数指定的毫秒数已经过去为止 |
| void notify() | 用于唤醒等待的单个线程 |
| void notifyAll() | 用于唤醒等待的所有线程 |
```java
package com.lagou.task18;
/**
* @author lijing
* @date 2020/10/15 15:55
* @description
*/
public class ThreadCommunicationTest implements Runnable{
private int cnt=1;
@Override
public void run() {
while (true){
synchronized (this) {
//每当有一个线程进来后先大喊一声,把其他喊起来,调用notify()
notify();
if(cnt<=100){
System.out.println("线程:" + Thread.currentThread().getName() + "中,cnt=" + cnt);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
cnt++;
//当前线程打印完毕一个整数后,为了防止继续打印下一个数据,则调用wait方法
try {
wait();//当前线程进入阻塞状态,自动释放对象锁,必须在锁定的代码中调用
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{break;}
}
}
}
public static void main(String[] args) {
ThreadCommunicationTest tct=new ThreadCommunicationTest();
Thread t1=new Thread(tct);
t1.start();
Thread t2=new Thread(tct);
t2.start();
}
}
生产者消费者模型的实现
package com.lagou.task18;
/**
* @author lijing
* @date 2020/10/15 16:20
* @description
*/
public class StoreHouse {
private int cnt=0;//用于记录产品的数量
public synchronized void produceProduct() {
notify();
if(cnt <10){
System.out.println("线程"+Thread.currentThread().getName()+"正在生产第"+(cnt+1)+"个产品...");
cnt++;
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consumerProduct() {
notify();
if(cnt>0){
System.out.println("线程" + Thread.currentThread().getName() + "正在消费第" + cnt + "个商品..");
cnt--;
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.lagou.task18;
/**
* @author lijing
* @date 2020/10/15 16:21
* @description
*/
public class ProduceThread extends Thread{
//声明一个仓库类型的引用作为成员变量,是为了能调用仓库类中的生产方法
//合成复用原则
private StoreHouse storeHouse;
public ProduceThread(StoreHouse storeHouse){
this.storeHouse=storeHouse;
}
@Override
public void run() {
while(true){
storeHouse.produceProduct();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.lagou.task18;
/**
* @author lijing
* @date 2020/10/15 16:25
* @description
*/
public class ConsumerThread extends Thread{
//声明一个仓库类型的引用作为成员变量,是为了能调用仓库类中的生产方法
//合成复用原则
private StoreHouse storeHouse;
public ConsumerThread(StoreHouse storeHouse){
this.storeHouse=storeHouse;
}
@Override
public void run() {
while(true){
storeHouse.consumerProduct();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.lagou.task18;
/**
* @author lijing
* @date 2020/10/15 16:26
* @description
*/
public class StoreHouseTest {
public static void main(String[] args) {
//创建仓库类的对象
StoreHouse storeHouse=new StoreHouse();
//创建线程类对象并启动
ProduceThread t1=new ProduceThread(storeHouse);
ConsumerThread t2=new ConsumerThread(storeHouse);
t1.start();
t2.start();
}
}