Java多线程--Lesson03

发布时间 2023-09-27 10:13:22作者: ~java小白~

线程同步

概念:

线程同步指的是在多个线程操作同一资源时,需要通过线程排队和线程锁来约束这些线程,使得其可以对其资源完成同步

并发指的是同一时间段内,有多个线程去操作同一个资源文件

由于同一进程的多个线程共享一块空间资源,带来方便的同时也带来了冲突问题,为了保证数据在方法中被访问的唯一性,在访问时加入锁机制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+"获得护摩之杖开始核爆"); } } } } }

 

如上代码:有两个对象,护摩和圣遗物,当每个人物要同时拿到这两个对象才可以进行核爆,不然就不能核爆,这两个对象全局唯一

由于两个线程同时开启,所以刚开始都拿到了一个对象,但是要两个对象同时拥有才可以核爆,所以都在等待对方先核爆完卸下对象,所以都核爆不了,这就是死锁

产生死锁的四个必要条件:

  1. 互斥条件:一个资源每次只能被一个进程使用
  2. 请求与保持条件:一个进程因请求而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:进程已获得资源,在未使用完前,不能强行剥夺
  4. 循环等待条件:若干进程形成一种首尾相连的循环等待资源关系

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;
    }
}

信号灯法,旨在利用标志位控制一些线程工作,然后另一些线程又被反向标志位控制

 线程池

背景:经常创建和销毁线程,使用量特别大的资源,比如并发情况下的线程,对性能影响很大

思路提前创建多个线程池,放入线程池中,使用时直接获取,使用完后放回线程池。可以频繁的创建和销毁,实现重复利用,类似生活中的交通工具

好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低了资源消耗(重复利用线程中的线程,不需要每次都创建)
  • 便于线程管理
  1. corePoolSize:核心池的大小
  2. maximumPoolSize:最大线程数
  3. 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;
    }
}