day15

线程的同步

问题的提出

卖票:现有三个窗口共同卖出100张票,模拟这个场景

package java2;

public class Window implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        while (true){
            if (ticket > 0){
                System.out.println("卖出第"+ticket+"张票");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket--;
            }else{
                break;
            }
        }
    }
}
package java2;

public class test {
    public static void main(String[] args) {
        Window w = new Window();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

可能会出现错票,重票等现象

理想的状态

在这里插入图片描述

极端状态

在这里插入图片描述

多线程出现了安全问题

问题:

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

解决:

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行

Synchronized

Java对于多线程的安全问题提供了专业的解决方式:同步机制

同步代码块:

synchronized (对象){

// 需要被同步的代码;

}

package java2;
/*
使用同步代码块解决继承Thread类的方式的线程安全问题

例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式

说明:在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
1.操作共享数据的代码,即为需要被同步的代码。  -->不能包含代码多了,也不能包含代码少了。
2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
*/
public class Window implements Runnable{
    private int ticket = 100;
    private Object obj = new Object();
    @Override
    public void run() {
        while (true){
            synchronized (obj) {
                if (ticket > 0) {
                    System.out.println("卖出第" + ticket + "张票");
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

同步方法

package java2;
/*
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
*/
public class Window implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        while (true){
                fun();
        }
    }
    public synchronized void fun(){
        if (ticket > 0) {
            System.out.println("卖出第" + ticket + "张票");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticket--;
        }
    }
}

同步的方式,解决了线程的安全问题。—好处

操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 —局限性

同步机制中的锁

synchronized的锁是什么?

任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。

同步方法的锁:静态方法(类名.class)、非静态方法(this)

同步代码块:自己指定,很多时候也是指定为this或类名.class

注意

必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全

一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

同步的范围

如何找问题,即代码是否存在线程安全?

  1. 明确哪些代码是多线程运行的代码
  2. 明确多个线程是否有共享数据
  3. 明确多线程运行代码中是否有多条语句操作共享数据

如何解决呢?(非常重要)

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

即所有操作共享数据的这些语句都要放在同步范围中

切记

范围太小:没锁住所有有安全问题的代码

范围太大:没发挥多线程的功能

释放锁的操作

当前线程的同步方法、同步代码块执行结束

当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。

当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。

当前线程在同步代码块、同步方法中执行了线程对象的**wait()**方法,当前线程暂停,并释放锁。

不会释放锁的操作

线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行

线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。

单例设计模式之懒汉式(线程安全)

package java2;

public class Singleton {
    Singleton s;
    private  Singleton(){
    }
    public Singleton getS() {
        if (s==null) {
            synchronized (Singleton.class) {
                if (s == null) {
                    s = new Singleton();
                }
            }
        }
        return s;
    }
}

线程的死锁问题

死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

package java2;

public class DeadLock {
    public static void main(String[] args) {
        String s1 = "111";
        String s2 = "222";
        MyThread myThread = new MyThread(s1,s2);
        MyThread2 myThread2 = new MyThread2(s2,s1);
        new Thread(myThread).start();
        new Thread(myThread2).start();
    }
}
class MyThread implements Runnable{
    Object obj1;
    Object obj2;

    public MyThread(Object obj1,Object obj2){
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    @Override
    public void run() {
        synchronized (obj1){
            try {
                Thread.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("1111");
            synchronized (obj2){
                System.out.println("2222");
            }
        }
    }
}
class MyThread2 implements Runnable{
    Object obj1;
    Object obj2;

    public MyThread2(Object obj1,Object obj2){
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    @Override
    public void run() {
        synchronized (obj1){
            try {
                Thread.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("1111");
            synchronized (obj2){
                System.out.println("2222");
            }
        }
    }
}

解决方法

  1. 专门的算法、原则
  2. 尽量减少同步资源的定义
  3. 尽量避免嵌套同步

Lock(锁)

从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁

package java2;

import java.util.concurrent.locks.ReentrantLock;

public class Window implements Runnable{
    private int ticket = 100;
    ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            lock.lock();
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName()+"卖出第" + ticket + "张票");
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket--;
                } else {
                    break;
                }
            lock.unlock();
        }
    }
}

synchronized Lock 的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

优先使用顺序:

Lock --> 同步代码块(已经进入了方法体,分配了相应资源) --> 同步方法(在方法体之外)

线程的通信

使用两个线程打印 1-100。线程1,线程2 交替打印

package java2;

public class Test01 implements Runnable{
    private int number = 1;
    @Override
    public  void run() {
        synchronized (this) {
            while (true) {
                if (number <= 100) {
                    this.notify();
                    System.out.println(Thread.currentThread().getName() + "打印" + number);
                    number++;
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
package java2;

public class Tets01test {
    public static void main(String[] args) {
        Test01 t = new Test01();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}

wait()与notify() 和 notifyAll()

wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行

notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待

notifyAll ():唤醒正在排队等待资源的所有线程结束等待

这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常

因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明

wait() 方法

  1. 在当前线程中调用方法: 对象名.wait()
  2. 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (或notifyAll) 为止。
  3. 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
  4. 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
  5. 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。

notify()/notifyAll()

  1. 在当前线程中调用方法: 对象名.notify()
  2. 功能:唤醒等待该对象监控权的一个/所有线程。
  3. 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

JDK5.0新增线程创建方式

实现Callable接口:

与使用Runnable相比, Callable功能更强大些

  1. 相比run()方法,可以有返回值
  2. 方法可以抛出异常
  3. 支持泛型的返回值
  4. 需要借助FutureTask类,比如获取返回结果
package java2;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallTest implements Callable {
    public static void main(String[] args) {
        FutureTask futureTask = new FutureTask(new CallTest());
        new Thread(futureTask).start();
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("qfdew");
    }
    @Override
    public Object call() throws Exception {
        return "1111";
    }
}

使用线程池

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

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

好处:

提高响应速度(减少了创建新线程的时间)

降低资源消耗(重复利用线程池中线程,不需要每次都创建)便于线程管理

corePoolSize:核心池的大小

maximumPoolSize:最大线程数

keepAliveTime:线程没有任务时最多保持多长时间后会终止

线程池相关API

JDK 5.0起提供了线程池相关API:ExecutorServiceExecutors

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable

Future submit(Callable task):执行任务,有返回值,一般又来执行Callable

  1. void shutdown() :关闭连接池
  2. Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
  3. Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
  4. Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
  5. Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
  6. Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
package java2;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//用线程池模拟死锁
public class DeadLock {
    public static void main(String[] args) {
        String s1 = "111";
        String s2 = "222";
        MyThread myThread = new MyThread(s1,s2);
        MyThread2 myThread2 = new MyThread2(s2,s1);
        ExecutorService ex = Executors.newFixedThreadPool(3);
        ex.execute(myThread);
        ex.execute(myThread2);
    }
}
class MyThread implements Runnable{
    Object obj1;
    Object obj2;

    public MyThread(Object obj1,Object obj2){
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    @Override
    public void run() {
        synchronized (obj1){
            try {
                Thread.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("1111");
            synchronized (obj2){
                System.out.println("2222");
            }
        }
    }
}
class MyThread2 implements Runnable{
    Object obj1;
    Object obj2;

    public MyThread2(Object obj1,Object obj2){
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    @Override
    public void run() {
        synchronized (obj1){
            try {
                Thread.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("1111");
            synchronized (obj2){
                System.out.println("2222");
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值