线程同步及同步块
结合上篇发布的文章,线程同步就算是讲完了。因为篇幅过长,所以这块被迫分成了两块
-
同步方法
由于private关键字来保证数据对象只能被方法所访问。所以我们只需要针对方法提出一套机制
synchronized关键字,包括synchronize方法和synchronized块
public synchronized void method(int args){}
-
synchronized方法控制对对象的访问,每个对象对应一把锁,每个对象都必须获得调用该方法的对象的锁才能执行,否则就进入阻塞。方法一旦执行,就独占该锁。
-
缺陷:若将一个大的方法声明为synchronized将会影响效率。
对于方法内的只读代码,是不需要锁的。修改内容才需要锁,锁太多了,会影响效率
-
修改为安全买票
package com.wang.syn1; /** * @author: 王海新 * @Date: 2021/2/28 16:40 * @Description: 修改为安全的买票, * 添加synchronized 锁 在buy方法上。就等于在buy的对象上设置了锁。 * 因为buy里面有延时,所以会让先进来的线程一直调用buy。如果票少,其它的线程就没有机会 *可以将延时放到run方法中,这样其它在buy执行完 */ public class SafeBuyTicket { public static void main(String[] args) { BuyTicket buyTicket = new BuyTicket(); new Thread(buyTicket,"小明").start(); new Thread(buyTicket,"小红").start(); new Thread(buyTicket,"小芳").start(); new Thread(buyTicket,"小蓝").start(); } } class BuyTicket implements Runnable{ //票 private int ticketNums = 10; boolean flage = true;//外部停止方法 @Override public void run() { //买票 while (flage) { try { Thread.sleep(1000); buy(); } catch (InterruptedException e) { e.printStackTrace(); } } } //添加synchronized 锁 private synchronized void buy() throws InterruptedException { //判断是否有票 if (ticketNums <= 0) { flage = false; return; } //模拟延时 //Thread.sleep(1000); //买票 System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums -- +"张"); } }
- 修改为安全取钱(这里的我也理解不了了,求大佬指导吧)
package com.wang.syn1; /** * @author: 王海新 * @Date: 2021/2/28 16:59 * @Description: 不安全的取钱,两个人去银行取钱 * */ public class SafeBank { public static void main(String[] args) { Account account = new Account(100,"结婚基金"); Drawing you = new Drawing(account, 50, "你"); Drawing girlFriend = new Drawing(account, 100, "grilFriend"); you.start(); girlFriend.start(); } } /********************************************************************************************************************* * @Author: 王海新 * @Date: 17:05 2021/2/28 * @Version: 1.0.0 * @Description: 账户 */ class Account{ String name; int money; public Account( int money,String name) { this.name = name; this.money = money; } } /********************************************************************************************************************* * @Author: 王海新 * @Date: 17:05 2021/2/28 * @Version: 1.0.0 * @Description: 银行 模拟取款 */ class Drawing extends Thread{ //账户 Account account; //取了多少钱 int DrawingMoney; //现在手里有多少钱 int nowMoney; //构造器,将变量初始化 Drawing(Account account,int DrawingMoney,String name){ super(name); this.account = account; this.DrawingMoney = DrawingMoney; } @Override public synchronized void run() { if (account.money - DrawingMoney < 0) {//判断账户中的钱是否够取 System.out.println(this.getName() + "钱不够,取不到"); return; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //更新取钱后账户钱的剩余 account.money = account.money - DrawingMoney; //更新用户手中的钱 nowMoney = nowMoney + DrawingMoney; System.out.println(account.name + "账户中的钱为" + account.money); //Thread.currentThread()就是返回一个Thread对象,所以我们可以用this System.out.println(this.getName()+ "手里的钱为" + nowMoney); } }
在run上加了锁,但是依然没有锁住。还是出现了负数,这里求解答?
-
同步块
-
同步块:synchronized(Obj){}
-
Obj称为同步监视器
- Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中,无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class(反射中讲解)
-
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问。
这里利用同步块,解决银行取钱问题
package com.wang.syn1; /** * @author: 王海新 * @Date: 2021/2/28 16:59 * @Description: 安全的取钱,两个人去银行取钱 * */ public class SafeBank { public static void main(String[] args) { Account account = new Account(1000,"结婚基金"); Drawing you = new Drawing(account, 50, "你"); Drawing girlFriend = new Drawing(account, 100, "grilFriend"); you.start(); girlFriend.start(); } } /********************************************************************************************************************* * @Author: 王海新 * @Date: 17:05 2021/2/28 * @Version: 1.0.0 * @Description: 账户 */ class Account{ String name; int money; public Account( int money,String name) { this.name = name; this.money = money; } } /********************************************************************************************************************* * @Author: 王海新 * @Date: 17:05 2021/2/28 * @Version: 1.0.0 * @Description: 银行 模拟取款 */ class Drawing extends Thread{ //账户 Account account; //取了多少钱 int DrawingMoney; //现在手里有多少钱 int nowMoney; //构造器,将变量初始化 Drawing(Account account,int DrawingMoney,String name){ super(name); this.account = account; this.DrawingMoney = DrawingMoney; } @Override public void run() { synchronized(account){ if (account.money - DrawingMoney < 0) {//判断账户中的钱是否够取 System.out.println(this.getName() + "钱不够,取不到"); return; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //更新取钱后账户钱的剩余 account.money = account.money - DrawingMoney; //更新用户手中的钱 nowMoney = nowMoney + DrawingMoney; System.out.println(account.name + "账户中的钱为" + account.money); //Thread.currentThread()就是返回一个Thread对象,所以我们可以用this System.out.println(this.getName()+ "手里的钱为" + nowMoney); } } }
课程上说。因为是账户执行的增删改,所以同步监视器要是account对象。(但是我还是不太动,有大佬可以详解一下吗?如果锁的是银行,那一直有一个人在里面取钱,岂不是也不会出错。当然结果是出错了。但是我真的不理解原因啊)
-
总结,锁的对象是要增删改的对象。
-
根据这个总结,我们将集合也该为安全的
package com.wang.syn1; import java.util.ArrayList; import java.util.List; /** * @author: 王海新 * @Date: 2021/3/3 09:22 * @Description: 线程安全的集合 * 造成数据不是10000个的原因有两个 *因循环已经跑完,程序结束,线程还没有将数据加入的情况。 * 两个线程同时操作同一个位置,造成的数据覆盖。 */ public class SafeList { public static void main(String[] args) { //创建一个集合 List<String> array = new ArrayList<String>(); //利用for循环,创建1000个线程向集合里面添加数据 for (int i = 0; i < 10000; i++) { new Thread( () -> { synchronized(array){ array.add(Thread.currentThread().getName()); } }).start(); } try {//利用阻塞。去除掉因循环已经跑完,程序结束,线程还没有将数据加入的情况。导致数据不一致的原因 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(array.size()); } }
- guc里面的一个安全类型的集合
package com.wang.syn1; import java.util.concurrent.CopyOnWriteArrayList; /** * @author: 王海新 * @Date: 2021/3/4 16:11 * @Description: 测试JUC安全类型的集合 */ public class TestGUC { public static void main(String[] args) { CopyOnWriteArrayList<String> strings = new CopyOnWriteArrayList<>(); for (int i = 0; i < 1000; i++) { new Thread( () -> { strings.add(Thread.currentThread().getName()); }).start(); } try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(strings.size()); } }
lock锁
package com.wang.syn2;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author: 王海新
* @Date: 2021/3/5 10:21
* @Description: 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 ticketNumber = 10;
private final ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
while (true){
//一般在加锁的代码块中,如果有异常抛出,就放到try{}finally{}中 在finally中将锁释放
try{
//显示的加锁
reentrantLock.lock();
if (ticketNumber > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNumber --);
}else {
break;
}
}
finally {
//显示的释放锁
reentrantLock.unlock();
}
}
}
}
程通信问题
生产者和消费者模式(这个模式并不是23种设计模式中的)
-
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
-
在这个问题中,synchronized可以阻止并发更新同一个资源
但是不能用来实现不同线程之间的消息传递
-
java提供了几个方法解决线程之间的通信问题
一下均是Object类的方法。都只能在同步方法,或者在同步代码块中使用,否者会抛出异常
- wait() 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
- wait(long timeout) 指定等待的毫秒数
- notify()唤醒一个处于等待状态的线程
- notifyAll()唤醒同一个对象上,所有调用wait方法的线程,优先级别高的线程,优先调用
-
解决方法1 生产者消费者模式–管程法
添加一个缓冲区,生产者生产的商品放到 这里。消费者从这里消费。从而实现协作
-
解决方法2 并发协作模式,信号灯法。可以用一个标志位来控制 如true放行 false等待
管程法
package com.wang.syn2;
/**
* @author: 王海新
* @Date: 2021/3/6 11:36
* @Description: 线程协作,管程法
* 生产者 消费者 产品 缓冲区
*/
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor( container) .start();
new Consumer(container).start();
}
}
/*********************************************************************************************************************
* @Author: 王海新
* @Date: 11:37 2021/3/6
* @Version: 1.0.0
* @Description: 生产者
*/
class Productor extends Thread {
SynContainer container;
public Productor(SynContainer container){
this.container = container;
}
//生产
public void run(){
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了"+ i + "只鸡。");
}
}
}
/*********************************************************************************************************************
* @Author: 王海新
* @Date: 11:38 2021/3/6
* @Version: 1.0.0
* @Description: 消费者
*/
class Consumer extends Thread {
SynContainer container;
public Consumer(SynContainer container){
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了--》" + container.pop().id+ "只鸡");
}
}
}
/*********************************************************************************************************************
* @Author: 王海新
* @Date: 11:38 2021/3/6
* @Version: 1.0.0
* @Description: 产品
*/
class Chicken {
int id;//产品编号
public Chicken(int id) {
this.id = id;
}
}
/*********************************************************************************************************************
* @Author: 王海新
* @Date: 11:39 2021/3/6
* @Version: 1.0.0
* @Description: 缓冲区
*/
class SynContainer{
//需要一个容器大小
Chicken[] chickens = new Chicken[10];
//容器计数器
int count = 0;
//生产者放入产品
public synchronized void push(Chicken chicken){
//如果容器满了,就等待消费者消费
if (count == chickens.length){
//通知消费者消费
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没有满,我们就需要丢入产品
chickens[count] = chicken;
count ++;
//可以通知消费者消费了
this.notifyAll();
}
//消费者消费商品
public synchronized Chicken pop(){
//判断能否消费
if (count == 0) {
//等待生产者生产
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果可以消费
count --;
Chicken chicken = chickens[count];
//吃完了,通知生产者生产
this.notifyAll();
return chicken;
}
}
这是草图,我自己哪里理清思路的时候随手画的。
信号灯法
package com.wang.syn2;
/**
* @author: 王海新
* @Date: 2021/3/6 11:36
* @Description: 线程协作,信号灯法
* 生产者 消费者 烤鸡店
*/
public class TestPC2 {
public static void main(String[] args) {
Chicken2 chicken2 = new Chicken2();
new Productor2(chicken2) .start();
new Consumer2(chicken2).start();
}
}
/*********************************************************************************************************************
* @Author: 王海新
* @Date: 11:37 2021/3/6
* @Version: 1.0.0
* @Description: 生产者
*/
class Productor2 extends Thread {
Chicken2 chicken;
public Productor2(Chicken2 chicken2 ){
this.chicken = chicken2;
}
//生产
public void run(){
for (int i = 0; i < 10; i++) {
chicken.pro(i);
}
}
}
/*********************************************************************************************************************
* @Author: 王海新
* @Date: 11:38 2021/3/6
* @Version: 1.0.0
* @Description: 消费者
*/
class Consumer2 extends Thread {
Chicken2 chicken;
public Consumer2(Chicken2 chicken2 ){
this.chicken = chicken2;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
chicken.con();
}
}
}
/*********************************************************************************************************************
* @Author: 王海新
* @Date: 11:38 2021/3/6
* @Version: 1.0.0
* @Description: 烤鸡店
*/
class Chicken2 {
int id;//产品编号
//生产完成,通知消费 false
//消费完成,通知生产 true
boolean flag = true;
//生产
public synchronized void pro(int id){
//判断是否生产
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//生产了
System.out.println("生产了第"+ id + "只鸡");
//通知消费
this.notifyAll();
this.id = id;
this.flag = !this.flag;
}
//消费
public synchronized void con(){
//判断是否消费
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费
this.id = id;
System.out.println("消费了"+ this.id + "只鸡。");
this.flag = !this.flag;
this.notifyAll();
}
}
利用一个标志位,达到两个线程协同的效果
线程池
- 经常创建和销毁特别大的资源,比如并发情况下的线程。对性能影响很大。
- 提前创建好多个线程,放入线程池,使用时直接获取。使用完放回池子中。避免频繁的创建和销毁,实现重复利用。
- 优点
- 提高响应速度(减少创建的时间)
- 降低资源消耗
- 便于线程管理
- corePoolSize:核心池大小
- maximumPoolSize :最大线程数
- KeepAliveTime :线程没有任务后,最多保存的少时间后销毁。
package com.wang.syn2;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author: 王海新
* @Date: 2021/3/8 15:51
* @Description: 线程池
*/
public class TestPool {
public static void main(String[] args) {
//1.创建服务,创建线程池
//newFixedThreadPool 参数为线程池大小
ExecutorService service = Executors.newFixedThreadPool(10);
//执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//关闭
service.shutdown();
}
}
/*********************************************************************************************************************
* @Author: 王海新
* @Date: 15:51 2021/3/8
* @Version: 1.0.0
* @Description:
*/
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
- OK,到这里,多线程的学习文章就发完了。 完结散花