Java多线程--2

四、线程同步

为什么需要线程同步?

  • 多个线程执行的不确定性引起执行结果的不稳定;
  • 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。

例子:创建三个窗口卖票,总票数为100张
存在线程的安全问题(错票,重票)

class windows1 implements Runnable{

    private int ticket = 100;
    @Override
    public void run() {
        while (true){
            if (ticket > 0){
                try {
                    Thread.sleep(100);//加了sleep之后,加大了出现票数0和-1的概率。
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else break;
        }
    }
}


public class WindowTest1 {
    public static void main(String[] args) {
        var windows = new windows1();

        var t1 = new Thread(windows);
        var t2 = new Thread(windows);
        var t3 = new Thread(windows);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        t1.start();
        t2.start();
        t3.start();

    }
}

问题:卖票过程中程序出现了重票和错票的情况------>出现了线程的安全问题
原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票,,导致了重票和错票的问题。
解决:在一个线程在操作数据过程中,其他线程不能参与进来,只有操作完数据,其他线程才能参与进来。这种情况,即使这个线程出现了阻塞,也不能被改变。
Java当中,我们通过同步机制来解决线程的安全问题

同步的方式解决了线程的安全问题,但是操作同步代码块时,只能有一个线程参与,其他线程等待。相当于时一个单线程的过程,效率相对低一些。

方法一:同步代码块

synchronized(同步监视器){
    //需要被同步的代码,即操作共享数据的代码,即为需要被同步的代码
     }
  • 共享数据:多个线程共同操作的数据例如上面的ticket
  • 同步监视器(俗称,锁):任何一个类的对象都可以充当锁。(注意:该对象不能为匿名的,且不能在包含同步代码块的方法中声明)
    • 多个线程必须要共用同一把锁,即对象不能为匿名的,且不能在包含同步代码块的方法中声明,如果使用当前类的对象充当锁,需要用当前类名.class来充当锁,否则会出现栈溢出的异常
    • 在继承Thread类这种线程的创建方式中,锁 的对象必须是静态的,即使用同一把锁
    • 在实现Runnable接口创建线程的同步代码块中,锁可以用this来代替(是同一个对象),但是在继承Thread类这种线程的创建方式中,锁不能使用this(此时的this是三个不同的对象)

示例代码:

class windows1 implements Runnable {

    private int ticket = 100;

    Object obj = new Object();

    @Override
    public void run() {

        //注意匿名对象不可以
        while (true) {
            synchronized (obj) {//注意锁要放在循环体内部,否则线程一知占用CPU
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else break;
            }
        }
    }
}

方法二:同步方法

如果操作共享数据的代码完整的声明在一个方法中。我们不妨将此方法声明为同步的。

实现Runnable接口的线程安全问题:

package JavaSE.Thread.exr;
/*
例子:创建三个窗口卖票,总票数为100张
存在线程的安全问题

方式二:同步方法:
解决实现Runnable接口的线程安全问题:
 */

class windows3 implements Runnable {

    private int ticket = 100;

    Object obj = new Object();

    @Override
    public void run() {
        while (ticket > 0){
            show();
        }
    }

    private synchronized void  show(){//同步监视器就是this
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}


public class Window_Test3 {
    public static void main(String[] args) {
        var windows = new windows3();

        var t1 = new Thread(windows);
        var t2 = new Thread(windows);
        var t3 = new Thread(windows);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        t1.start();
        t2.start();
        t3.start();

    }
}

使用同步方法来处理继承Thread类的线程安全问题:

同步方法必须是静态的

package JavaSE.Thread.exr;

/*
使用同步方法来处理继承Thread类的线程安全问题
 */

class windows4 extends Thread {

    private static int ticket = 100;//static 静态变量可以让三个线程的总票数为100张,不加static总票数为300张。
    private static Object obj = new Object();

    @Override
    public void run() {
        //不是同一把锁,出现了线程的安全问题
        while (ticket > 0) {
            show();
        }
    }

    private static synchronized void show(){//同步监视器this,但是this对象有三个,所以方法必须是静态的
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

public class Window_Test4 {
    public static void main(String[] args) {
        var win1 = new windows4();
        var win2 = new windows4();
        var win3 = new windows4();

        win1.start();
        win2.start();
        win3.start();
    }
}

关于同步方法的总结:

  1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明;
  2. 非静态的同步方法,同步监视器是:this(参考上面解决实现Runnable接口的线程安全问题)
  3. 静态的同步方法,同步监视器是:当前类本身(参考使用同步方法来处理继承Thread类的线程安全问题)

线程的死锁问题

死锁:

  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

死锁的例子:

package JavaSE.Thread;

/**
 * 演示线程的死锁问题
 * 
 */
public class Thread_Test4 {
    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();


    }
}

死锁的解决方法:

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量减免嵌套同步

方法三:jdk5新增 Lock锁

步骤:

  1. 实例化ReentrantLock
  2. 调用锁定方法lock()
  3. 调用解锁方法:unlock()

示例代码:

package JavaSE.Thread;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 解决线程安全问题之三:Lock锁
 */
class windows4 implements Runnable {

    private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock(true);//true 代表公平的,先进先出  false代表随机的
    @Override
    public void run() {

        //注意匿名对象不可以
        while (true) {
            try {
                //2.调用锁定方法lock()
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else break;
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }
        }
    }
}


public class Lock_Test{
    public static void main(String[] args) {
        var windows = new windows4();

        var t1 = new Thread(windows);
        var t2 = new Thread(windows);
        var t3 = new Thread(windows);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        t1.start();
        t2.start();
        t3.start();

    }
}


synchronized(同步代码块,同步方法)与lock的异同?

  • 相同点:二者都可以解决线程的安全问题
  • 不同点sychronized机制在执行完相应的同步代码以后,自动的释放同步监视器。而lock需要手动启动同步(lock()),同时结束同步也需要手动的实现unlock()

五、线程通信问题

涉及到的三个方法

  1. wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
  2. notify():一旦执行此方法,就会唤醒wait的一个线程。如果有多个线程被wait,就唤醒优先级高的线程
  3. notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

注意:

  1. wait()、notify()、notifyAll()三个方法必须使用在同步代码块或同步方法中。Lock锁中都不可以使用。
  2. wait()、notify()、notifyAll()三个方法的调用者,必须是同步代码块或同步方法中的监视器发起的调用,否则会发生异常。
  3. wait()、notify()、notifyAll()三个方法是定义在java.lang.Object类中

使用举例:

package JavaSE.Thread;


class Number implements Runnable {
    private static int number = 1;

    @Override
    public void run() {

        while (true) {
            synchronized (this) {
                notifyAll();
                if (number <= 100) {


                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                } else break;
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

public class CommunicationTest {

    public static void main(String[] args) {
        var num = new Number();

        var t1 = new Thread(num);
        var t2 = new Thread(num);
        var t3 = new Thread(num);

        t1.setName("线程一");
        t2.setName("线程二");
        t3.setName("线程三");


        t1.start();
        t2.start();
        t3.start();
    }
}

sleep()和wait()方法的异同

  • 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态;
  • 不同点:
    • 两个方法声明的位置不同,Thread类中声明sleep(),Object类中声明wait()
    • 调用的要求不同:sleep()可以在任何需要的场景下调用。wait()必须在同步代码块或同步方法中调用
    • 关于释放同步监视器的问题:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

生产者,消费者问题:

生产者将产品交给店员,而消费者从店员处取走商品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位房产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

这里可能会出现两个问题:

  1. 生产者比消费者快时,消费者会漏掉一些数据没有取到;
  2. 消费者比生产者快时,消费者会取相同的数据。

代码:

package JavaSE.Thread.exr;


class Clrek{

    private static int num = 0;

    public static int getNum() {
        return num;
    }

    public static void setNum(int num) {
        Clrek.num = num;
    }

    //生产产品
    public synchronized void producerProduct(){
        if (num < 20){
            num++;
            System.out.println(Thread.currentThread().getName() + "生产产品   当前产品数量" + this.getNum());

            notify();//Object类的方法
        }
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //消费产品
    public synchronized void consumerProduct(){
        if (num > 0){
            num--;
            System.out.println(Thread.currentThread().getName() + "消费产品   当前产品数量" + this.getNum());

            notify();
        }

        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

class Producer extends Thread{

    private Clrek clrek;

    public Producer(Clrek clrek){
        this.clrek = clrek;
    }
    @Override
    public void run() {
        while (true){
            try {
                sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clrek.producerProduct();
        }
    }
}

class Consumer extends Thread{
    private  Clrek clrek;
    public Consumer(Clrek clrek){
        this.clrek = clrek;
    }
    @Override
    public void run() {
        while (true){
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clrek.consumerProduct();
        }
    }
}
public class Product_Test {

    public static void main(String[] args) {
        Clrek clrek = new Clrek();

        Producer p1 = new Producer(clrek);
        p1.setName("生产者一:");

        Consumer c1 = new Consumer(clrek);
        c1.setName("消费者一:");

        p1.start();
        c1.start();
    }
}

六、高级主题jdk5.0新增的线程创建方式

新的创建方式之一:实现Callable接口

与使用Runnable接口相比,Callable功能更加强大:

  • 相比run()方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助Future Task类,比如获取返回的结果
    • Future接口
      • 可以对具体Runnable/Callable任务的执行结果进行取消、查询是否完成、获取结果
      • FutureTask是Future接口的唯一实现类
      • FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行、又可以作为Future得到Callable的返回值。

创建步骤:

  1. 创建一个实现Callable接口的实现类
  2. 实现call方法,将此线程需要执行的操作声明在call()中。
  3. 创建Callable接口实现类的对象
  4. 将此Callable接口实现类的对象,作为参数传递到FutureTask的构造器中,创建FutureTask的对象
  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法。
  6. 获取Callable接口中call()的返回值(可以不获取)

示例代码:

package JavaSE.Thread;


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

//创建一个实现Callable接口的实现类
class NumThread implements Callable{
    //实现call方法,将此线程需要执行的操作声明在call()中。
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}
public class Callable_Test {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        var numT = new NumThread();
        //4.将此Callable接口实现类的对象,作为参数传递到FutureTask的构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numT);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法。
        new Thread(futureTask).start();

        try {
            //6.获取Callable接口中call()的返回值
            //返回值为构造器参数Callable实现类重写的call()的返回值。
            Object num = futureTask.get();
            System.out.println(num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

新的创建方式之二:使用线程池

  • **背景:**经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
  • **思路:**提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁的吹昂见销毁、实现重复利用。类似生活中的公共交通工具。
  • 好处:
    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
    • 便于线程管理
      • **corePoolSize:**核心池的大小
      • **maximumPoolSize:**最大线程数
      • **keepAliveTime:**线程没有任务时最多保持多长时间后会终止

线程池相关API

  • JDK 5.0起提供了线程池相关API:ExecutorService 和 Exocutors
    • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
      • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
      • Futuresubmit(Callable task):执行任务,有返回值,一般又来执行Callable
      • void shutdown():关闭线程池连接
    • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
      • Executors.newCachedThreadPool():创建可根据需要创建新线程的线程池
      • Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
      • Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
      • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期的执行
package JavaSE.Thread;

import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class ThreadPool_Test {
    public static void main(String[] args) {
        //1.提供指定线程数量的线程池
        var executorService = Executors.newFixedThreadPool(10);

        //设置线程池的属性
        var sevice = (ThreadPoolExecutor) executorService;
        sevice.setCorePoolSize(1);

        //适用于Runnable
        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象。
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    if (i % 2 == 0){
                        System.out.println(Thread.currentThread().getName() + "====" +i);
                    }
                }
            }
        });


        //适合使用于Callable
        executorService.submit(new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                for (int i = 1; i <= 100; i++) {
                    if (i % 2 != 0){
                        System.out.println(Thread.currentThread().getName() + "====" +i);
                    }
                }
                return null;
            }
        });
        //3.关闭线程池
        executorService.shutdown();
    }
}

设置线程池的属性:

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值