线程同步
概念:
线程同步指的是在多个线程操作同一资源时,需要通过线程排队和线程锁来约束这些线程,使得其可以对其资源完成同步
并发指的是同一时间段内,有多个线程去操作同一个资源文件
由于同一进程的多个线程共享一块空间资源,带来方便的同时也带来了冲突问题,为了保证数据在方法中被访问的唯一性,在访问时加入锁机制synchronized,当一个线程获得排他锁,独占资源,其它线程必须等待,释放锁后才可以使用
- 一个线程拥有锁,则其它需要此锁的线程必须挂起等待
- 在线程竞争环境激烈的情况下,加锁,释放锁会导致频繁的上下问文切换和调度延时,引起性能问题
- 如果一个优先级高的线程的等待一个优先级低的释放锁,会导致优先级倒置,引起性能问题
三大线程不安全示例
第一种:多个线程去竞争同一资源
在资源有限的时候,很明显可以发现有一个问题那就是,在拿取最后一个资源的时候,都以为自己可以拿到,所以去操作了这个资源,就导致资源下溢为了负数
代码展示:
//不安全的买票,票数为 0或者 -1 public class Unsafe1 { public static void main(String[] args) { BuyTicket station = new BuyTicket(); new Thread(station,"魈").start(); new Thread(station,"胡桃").start(); new Thread(station,"钟离").start(); } } class BuyTicket implements Runnable { private int tickets = 10; Boolean flag = true; @Override public void run() { while (true){ try { buy(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } public void buy() throws InterruptedException { if (tickets<0){ flag=false; return; } Thread.sleep(10); System.out.println(Thread.currentThread().getName()+"拿到了第"+tickets--); } }
//输出结果 钟离拿到了第10 魈拿到了第8 胡桃拿到了第9 魈拿到了第6 钟离拿到了第5 胡桃拿到了第7 魈拿到了第4 胡桃拿到了第3 钟离拿到了第4 钟离拿到了第2 胡桃拿到了第1 魈拿到了第1 钟离拿到了第0 魈拿到了第-1
第二种:多线程竞争单一资源
当多个线程取竞争单个资源的时候,会都拿到此资源的初始值,然后操作后结果会是很奇怪或者超出溢值的,这是因为每个线程都有独属于自己的运算内存,它们相互分开互不干扰
代码展示:
package Multihead; public class Unsafe2 { public static void main(String[] args) { Account account = new Account(100, "结婚基金"); Drowing you = new Drowing(account, 50, "你"); Drowing me = new Drowing(account, 70, "我"); you.start(); me.start(); } } class Account{ int money; String name; public Account(int money, String name) { this.money = money; this.name = name; } public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } public String getName() { return name; } public void setName(String name) { this.name = name; } } class Drowing extends Thread{ Account account; int drawingMoney; int nowMoney; String name; public Drowing( Account account, int drawingMoney, String name) { this.account = account; this.drawingMoney = drawingMoney; this.name = name; } @Override public void run() { if (account.money-drawingMoney<0){ System.out.println(this.getName()+"钱不够了"); return; } try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } //卡内余额 - 取的钱 account.money=account.money-drawingMoney; nowMoney=nowMoney+drawingMoney; System.out.println(account.getName()+"余额为:"+account.money); System.out.println(this.getName()+"手里的钱:"+nowMoney); } }
//运行结果 结婚基金余额为:-20 结婚基金余额为:-20 Thread-0手里的钱:50 Thread-1手里的钱:70
可以看到,两个都对初始值100,操作了,导致余额本来是取不出来的,但是他们的操作中,先拿到数据的还没写回,后一个线程就又开始操作了
第三种:多个线程顺序写资源时会发生覆盖现象
这种情况发生在当我们的资源是连续的时候,当线程拿到同一资源时,有的先写回,有的后写回,由于线程的算法一致,这就导致后写回的数据覆盖了先写回的数据
代码展示:
public class Unsafe3 { public static void main(String[] args) { List<String> list = new ArrayList<String>(); for (int i = 0; i < 1000; i++) { new Thread(()->{ list.add(Thread.currentThread().getName()); }).start(); } System.out.println(list.size()); } } // 输出结果:999 比预计的少了一个
这个是数据量越大,越明显,被覆盖的越多
线程同步操作
在Java中我们可以使用privtae修饰符来修饰属性变为不可访问,相同的也可以使用针对方法的一套机制,这套机制就是synchronized关键字,它有两种用法:synchronized方法和synchronized块
同步方法:
public synchronized void method() { }
synchronized控制每个对象的访问,每个对象都有一把自己的锁,每个synchronized方法都必须要获得锁以后才可以执行,否则就会阻塞
并且每个synchronized方法执行的时候都会独占一把锁,直到方法结束以后才会释放锁,后面的线程才能获得锁
缺陷:如果一个很大的synchronized方法获得锁会非常影响效率
代码示例:
public synchronized void buy() throws InterruptedException { if (tickets<=0){ flag=false; return; } Thread.sleep(10); System.out.println(Thread.currentThread().getName()+"拿到了第"+tickets--); }
加了synchronized关键字的方法会监视此方法中的对象,对象中所有的对象都只能在线程有锁的情况下执行
同步块
同步块指监视此区域中的某个对象。
同步块:
synchronized(obj){ //代码块 }
obj:称之为同步监视
obj可以是任何对象,但是推荐使用共享资源作为同步监视器
同步方法中无需指定监视对象,因为同步方法的同步监视器就是 this ,就是这个对象本身,或者是class
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
代码展示:
@Override public void run() { //此方法是操作的代码,并不是account的拥有者 synchronized (account){ //使用synchronized同步块,绑定实际要操作的对象account,这里是也就是账户 if (account.money-drawingMoney<0){ System.out.println(this.getName()+"钱不够了"); return; } try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } //卡内余额 - 取的钱 account.money=account.money-drawingMoney; nowMoney=nowMoney+drawingMoney; System.out.println(account.getName()+"余额为:"+account.money); System.out.println(this.getName()+"手里的钱:"+nowMoney); }
}
使用同步块可以监视唯一对象,它和同步方法都是可以实现同步上锁,只是上锁的对象一个是自定义,一个是默认this
死锁
多个线程各自占有一些共用资源,并且互相等待其它线程占有的资源释放后才能运行。而导致两个或多个线程在处于等待资源的状态,都停止的状态
某一个同步块需要同时拥有两个以上的对象的锁时,可能发生“死锁”问题
代码展示:
package Multihead; public class DeadLock { public static void main(String[] args) { HeBao hu = new HeBao("胡桃"); HeBao xiao = new HeBao("魈"); hu.start(); xiao.start(); } } //护摩之杖 class HuMo{ } //270专用圣遗物 class ShenYiWu{ } //核爆手法 class HeBao extends Thread{
//使用static关键字修饰,保持全局唯一 static HuMo huMo =new HuMo(); static ShenYiWu shenYiWu=new ShenYiWu(); String name;//装备人物 public HeBao(String name) { this.name = name; } @Override public void run() { try { StartHeBao(); } catch (InterruptedException e) { throw new RuntimeException(e); } } public void StartHeBao() throws InterruptedException { if (name.equals("魈")){ synchronized (huMo){ System.out.println(name+"获得护摩之杖"); Thread.sleep(100); synchronized (shenYiWu){ System.out.println(name+"获得圣遗物开始核爆"); } } }else { synchronized (shenYiWu){ System.out.println(name+"获得圣遗物"); Thread.sleep(100); synchronized (huMo){ System.out.println(name+"获得护摩之杖开始核爆"); } } } } }
如上代码:有两个对象,护摩和圣遗物,当每个人物要同时拿到这两个对象才可以进行核爆,不然就不能核爆,这两个对象全局唯一
由于两个线程同时开启,所以刚开始都拿到了一个对象,但是要两个对象同时拥有才可以核爆,所以都在等待对方先核爆完卸下对象,所以都核爆不了,这就是死锁
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得资源,在未使用完前,不能强行剥夺
- 循环等待条件:若干进程形成一种首尾相连的循环等待资源关系
Lock锁(自定义锁)
从jdk5.0开始,Java提供了强大的线程同步机制------通过显式定义同步锁对象实现同步,同步锁使用Lock对象来充当
java.util.concurrent.locks.Lock接口控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock锁
ReentrantLock实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrentLock,可以显式加锁,释放锁
代码展示:
public void buy() throws InterruptedException { try { lock.lock(); if (tickets<=0){ flag=false; return; } Thread.sleep(10); System.out.println(Thread.currentThread().getName()+"拿到了第"+tickets--); }finally { lock.unlock(); } }
加锁和释放锁的语句一般写在try-catch语句中,上锁的语句都写在try{ }代码块,释放锁写在finally{ }代码块
synchronized和Lock对比:
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
Lock只有代码锁块,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费少量的时间来调度线程,性能更好,并且具有更好的拓展性
优先使用顺序:
Lock > 同步代码块 > 同步方法
线程通信
线程通信是用于解决多线程的生产者与消费者的问题,当不同的线程之间所履行的职责不同的时候,有的线程负责生产,有的负责消费,它们不能协调很容易导致生产过多,或没有资料的问题
线程通信使得两个或多个线程可以相互交流,协同生产
Java提供了几个线程通信的问题
- wait():表示线程一直等待,直到其它线程通知,与sleep不同,会释放锁
- wait(long timeout):指定等待的毫秒数
- notify():唤醒一个处于等待的线程
- notifyAll():唤醒同一个对象上所有使用wait()方法的线程,优先级别高的线程优先调度
注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常
解决线程通信的方法
第一种:管程法
- 生产者:负责生产数据的模块
- 消费者:负责处理数据的模块
- 缓冲区:消费者不能直接使用生产者的数据,它们之间有一个缓冲区
生产者将生产的数据放在缓冲区里,消费者从缓冲区中拿
代码展示:
package Multihead; public class TestPC { public static void main(String[] args) { SynContainer container = new SynContainer(); new Productor(container).start(); new Consumer(container).start(); } } //生产者 class Productor extends Thread{ SynContainer synContainer; public Productor(SynContainer synContainer) { this.synContainer = synContainer; } //生产产品 @Override public void run() { for (int i = 0; i < 100; i++) { synContainer.push(new Chicken(i)); System.out.println("生产了"+i+"只鸡"); } } } //消费者 class Consumer extends Thread{ SynContainer synContainer; public Consumer(SynContainer synContainer) { this.synContainer = synContainer; } //消费产品 @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("消费了----"+synContainer.pop().id+"只鸡"); } } } //产品 class Chicken{ int id; public Chicken(int id) { this.id = id; } } class SynContainer{ Chicken[] chickens= new Chicken[10]; int count =0; //生产产品到 10个,否则不能消费 public synchronized void push(Chicken chicken){ if (count== chickens.length){ try { this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } chickens[count]=chicken; count++; this.notifyAll(); } //消费产品,当没有产品时,通知生产者生产 public synchronized Chicken pop(){ if (count==0){ //等待 try { this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } count--; Chicken chicken=chickens[count]; this.notifyAll(); return chicken; } }
第二种:信号灯法
设置一个标志位,利用标志位控制线程的启动和停止
代码展示:
package Multihead; public class TestPC2 { public static void main(String[] args) { new go().start(); new go().start(); } } class go extends Thread{ Boolean flag = false; int count=0; public go() { } @Override public void run() { try { for (count = 0; count < 10; count++) { Tv tv = new Tv(flag); flag=tv.keep(); } } catch (InterruptedException e) { throw new RuntimeException(e); } } } class Tv{ Boolean flag; public Tv(Boolean flag) { this.flag = flag; } public synchronized Boolean keep() throws InterruptedException { Thread.sleep(200); if (flag){ System.out.println("boy拿到了"); }else { System.out.println("girl拿到了"); } flag = !flag; return flag; } }
信号灯法,旨在利用标志位控制一些线程工作,然后另一些线程又被反向标志位控制
线程池
背景:经常创建和销毁线程,使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路提前创建多个线程池,放入线程池中,使用时直接获取,使用完后放回线程池。可以频繁的创建和销毁,实现重复利用,类似生活中的交通工具
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低了资源消耗(重复利用线程中的线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间会终止
JDK5.0提供了线程相关的API:ExecutorService和Executors
ExecutorService:真正的线程接口,常见的子类,TheadPoolExecutor
- void execute(Runnable command):执行任务命令,没有返回值,一般用来执行Runnable
- <T> Future<T> submit ( callable<T > task):执行任务,有返回值,一般又来执行callable
- void shutdown():关闭链接池
Executors:工具类,线程池的工厂类,用于创建并返回不同类型的线程池
代码展示:
package Multihead; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestPool { public static void main(String[] args) { //创建线程池的大小 ExecutorService service = Executors.newFixedThreadPool(3); //从线程池拿取线程执行 service.execute(new myThead()); service.execute(new myThead()); service.execute(new myThead()); service.execute(new myThead()); //使用完关闭线程 service.shutdown(); } } class myThead implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()); try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } } }
如上:在创建线程池时,只创建了三条线程,但是执行的时候却有4个任务需要使用,所以最后一个任务肯定要其它线程执行完后,才会被执行
//输出结果 pool-1-thread-3 pool-1-thread-1 pool-1-thread-2 pool-1-thread-3
如上,线程3先执行完,所以他多执行了一个任务
回顾线程创建的三种方式:
package Multihead; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class TheadNew { public static void main(String[] args) throws ExecutionException, InterruptedException { // 继承Thread类的运行方式 new Thead1().start(); // 实现Runnable接口的运行方式 new Thread(new Thead2()).start(); // 实现callable接口的运行方式 FutureTask<Integer> task = new FutureTask<>(new Thead3()); new Thread(task).start(); Integer i = task.get(); System.out.println(i); } } // 继承Thead类 class Thead1 extends Thread{ @Override public void run() { System.out.println("继承Thead类"); } } //实现Runnable接口 class Thead2 implements Runnable{ @Override public void run() { System.out.println("实现Runnable接口"); } } //实现callable接口 class Thead3 implements Callable<Integer>{ @Override public Integer call() throws Exception { System.out.println("实现callable接口"); return 100; } }
标签:Java,synchronized,--,void,class,线程,new,多线程,public From: https://www.cnblogs.com/5ran2yl/p/17729763.html