【Java多线程 34】

一、线程的死锁问题

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

  解决方法

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步
package com.csii.day03;

/**
 * @Author wufq
 * @Date 2020/12/22 10:55
 * 死锁演示
 */

class A{

    public synchronized void foo(B b){
        System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入A实例的foo方法");

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

        System.out.println("当前线程名:"+Thread.currentThread().getName()+"企图调用B实例的bar方法");

        b.last();
    }

    public synchronized void last(){
        System.out.println("进入了A类的内部last方法");
    }
}




class B{

    public synchronized void bar(A a){
        System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入B实例的bar方法");

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

        System.out.println("当前线程名:"+Thread.currentThread().getName()+"企图调用A实例的foo方法");

        a.last();
    }


    public synchronized void last(){
        System.out.println("进入了B类的内部last方法");
    }

}


public class BeadLock implements Runnable{

    A a = new A();
    B b = new B();

    public void init(){
        Thread.currentThread().setName("主线程");
        a.foo(b);
        System.out.println("进入主线程之后");
    }

    public void run(){
        Thread.currentThread().setName("副线程");
        b.bar(a);
        System.out.println("进入副线程之后");
    }

    public static void main(String[] args){

        BeadLock bl = new BeadLock();
        new Thread(bl).start();
        bl.init();


    }
}

二、解决线程安全问题的方式三:lock锁  ----   JDK5.0新增

  • 从JDK5.0开始,Java提供了更强大的线程同步机制---通过显式定义同步锁对象来实现同步。同步锁使用lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁。
package com.csii.day03;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author wufq
 * @Date 2020/12/22 15:14
 * lock锁解决线程安全问题
 */



class Window5 implements Runnable{

    private int ticket = 100;

    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                //调用lock()方法,获取同步监视器
                lock.lock();

                Thread.sleep(100);
                if(ticket>0){
                    System.out.println(Thread.currentThread().getName()+"票号:"+ticket);
                    ticket --;
                }else {
                    break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {

                //调用解锁方法
                lock.unlock();
            }
        }

    }
}


public class LockTest {
    public static void main(String[] args){

        Window5 w5=new Window5();
        for(int i=0;i<3;i++) {
            Thread tt= new Thread(w5);
            tt.setName("窗口"+i+" ");

            tt.start();
        }


    }

}

synchronized与Lock的对比

1、Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放

2、Lock只有代码块锁,synchronized有代码块锁和方法锁

3、使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

优先使用顺序:

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

三、练习1

银行有一个账户

有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。

问题:该程序是否有安全问题,如果有,如何解决?

【提示】

1、明确哪些代码是多线程运行代码,须写入run()方法

2、明确什么是共享数据

3、明确多线程运行代码中哪些语句是操作共享数据的

package com.csii.day03;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author wufq
 * @Date 2020/12/22 17:20
 * 银行有一个账户
    有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。

    分析:
    是否涉及到多线程? 两个储户线程
    是否有共享数据?  账户或者余额是共享数据
    是否存在线程安全问题? 有线程安全问题
    如何解决?(同步机制:三种方法)
 */


//账户类
class Account{

    //定义账户
    private double balance;

    private ReentrantLock lock = new ReentrantLock();

    public Account(double balance) {
        this.balance = balance;
    }

    //amt是存的钱数
    public void deposit(double amt) {
        try {

            //调用锁定方式lock()
            lock.lock();
            if(amt>0){

                balance += amt;
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName()+"存钱成功,余额为:"+balance);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //调用解锁方式unlock()
            lock.unlock();
        }

    }
}

//储户
class Customer implements Runnable{

    private Account acc;

    public Customer(Account acc) {
        this.acc = acc;
    }

    @Override
    public void run() {

        for(int i=0;i<3;i++) {
            acc.deposit(1000);
        }
    }
}



public class AccountTest {
    public static void main(String[] args){

        Account acc = new Account(0);
        Customer c1 = new Customer(acc);
        Customer c2 = new Customer(acc);

        Thread t1 = new Thread(c1);
        t1.setName("甲");

        Thread t2 = new Thread(c2);
        t2.setName("乙");

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

    }

}

====执行结果====
甲存钱成功,余额为:1000.0
甲存钱成功,余额为:2000.0
甲存钱成功,余额为:3000.0
乙存钱成功,余额为:4000.0
乙存钱成功,余额为:5000.0
乙存钱成功,余额为:6000.0

四、线程的通信

1、

 涉及到的三个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态
,并释放同步监视器
notify():一旦执行此方法,就会唤醒被wait的一个线程
如果多个线程被wait,就唤醒优先级高的那个
totifuAll():一旦执行此方法,就会唤醒被wait的所有线程


注意点:
1、wait(),notify(),notifyAll()三个方法
必须使用在同步代码块或同步方法中
2、wait(),notify(),notifyAll()三个方法的调用者必须
是同步代码块或者同步方法中的同步监视器,否则,
会出现IlllegalMonitorStateException异常
3、wait(),notify(),notifyAll()三个方法是定义在
java.lang.Object类中

package com.csii.day04;

/**
 * @Author wufq
 * @Date 2020/12/24 10:35
 *
 * 线程通信:使用两个线程打印1-100,线程1,线程2 交替打印
 */

class Num implements Runnable{

    private int number = 1;

    /*
    * 1、wait方法和notify方法其实前面是有this对象的,这就表明同步监视器,wait方法,notify方法是有被对象调用的,并且是谁调用对象就是谁
    * 2、这里把同步监视器(锁)设置成obj的对象,而wait和notify方法依然采用this方法,执行后会报IllegalMonitorStateException异常
    * 但是wait和notyfy方法都设置成obj对象,是可以运行成功的,这就表明:wait、notyfy、notyfyAll三个方法的调用必须是同步监视器内
    * 方法,否则会报异常
    *
    * */
    Object obj = new Object();

    @Override
    public void run() {

//        synchronized (this) {
        synchronized (obj) {
            while (true){

                //一旦执行此方法,就会唤醒被wait的一个线程,如果多个线程被wait,就唤醒优先级高的那个
//                this.notify();
                obj.notify();
                try {
                    Thread.sleep(100);
                    if(number<=100) {
                        System.out.println(Thread.currentThread().getName() + ":" + number);
                        number++;

                        //一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
//                       this.wait();
                        obj.wait();
                    }else {
                        break;
                    }
                } catch (Exception e) {
                        e.printStackTrace();
                }
                }
            }
        }

    }


public class Communication {

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

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

        t1.setName("线程1:");
        t2.setName("线程2:");

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

    }

}

=====执行结果=====
线程1::1
线程2::2
线程1::3
线程2::4
线程1::5
线程2::6

2、sleep和wait方法的异同

相同点:一旦执行方法,都可以使的当前的线程进入阻塞状态

不同点:

1)、两个方法声明的位置不同:Thread类中声明sleep()方法,Object类中声明wait()方法

2)、调用的要求不同:sleep可以在任意需要的场景下调用,wait必须使用在同步代码块或者同步方法中

3)、关于释放同步监视器:如果两个方法都使用在同步代码块或者同步方法中,sleep()不释放,wait()会释放   --即一旦使用了wait必须使用notify进行唤醒被阻塞的线程

3、线程通信的金典例题:生产者和消费者的问题

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

这里可能出现两个问题:

1)、生产者比消费者快时,消费者会漏掉一些数据没有收到

2)、消费者比生产者快时,消费者会取相同的数据

package com.csii.day04;

/**
 * @Author wufq
 * @Date 2020/12/25 16:20
 *
 */


//共享数据
class Clerk{

    private int productCount = 0;
    //生产的产品
    public synchronized void produceProduct() throws InterruptedException {

            if(productCount <20){

                productCount++;
                System.out.println(Thread.currentThread().getName()+"开始生产第"+productCount+"个");

                notify();

            }
            else {
                wait();
            }
    }

    //消费的产品
    public synchronized void customerProduct() throws InterruptedException {


            if(productCount >0){
                System.out.println(Thread.currentThread().getName()+"开始消费第"+productCount+"个");
                productCount--;

                notify();
            }else {
                wait();
            }
    }
}

//生产者和消费者是两个多线程
class Producer implements Runnable{

    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName()+"开始生产产品...");

        while (true){

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

        }

    }
}

class Customer implements Runnable{

    private Clerk clerk;

    public Customer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName()+"开始消费产品...");

        while (true){

            try {
                Thread.sleep(200);
                clerk.customerProduct();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }
}


public class ProductTest {
    public static void main(String[] args){

        Clerk clerk = new Clerk();

        Producer p1 = new Producer(clerk);
        Customer c1 = new Customer(clerk);

        Thread t1 = new Thread(p1);
        Thread t2 = new Thread(c1);

        t1.setName("生产者");
        t2.setName("消费者");

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

五、JDK5.0新增线程创建方式

新增方式一:实现Callable接口

1、与使用Runable相比,Callable功能更强大些

  • 相比run()方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果

2、Future接口

  • 可以对具体Runable、Callable任务的执行结果进行取消,查询是否完成,获取结果等
  • FutureTask是Future接口的唯一实现类
  • FutureTask同时实现了Runable,Future接口。它即可以作为Runable被线程执行,又可以作为Future得到Callable的返回值
package com.csii.day04;

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

/**
 * @Author wufq
 * @Date 2020/12/28 11:29
 */

//1、创建一个实现Callable接口的实现类
class NumTest implements Callable<Integer>{

//    2、实现call方法,将此线程需要执行的操作声明在call()中

    /*
    * 实现20以内偶数的和
    * */
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for(int i=0;i<20;i++){
            if(i%2==0){
                System.out.println(i);
                sum += i ;
            }
        }

        return sum;
    }
}


public class Test001 {
    public static void main(String[] args){
//    3、创建Callable实现类的对象
        NumTest numTest = new NumTest();

//    4、将此Callable实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask<Integer> task = new FutureTask<>(numTest);

//    5、将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法
        new Thread(task).start();

        try {
//    6、如果需要返回call方法的值,需要调用get方法来获取
            Integer sum = task.get();  // get()方法返回值即为FutureTask构造器Callable
                         实现类重写call()的返回值
System.out.println(
"总和:"+sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } ====执行结果==== 0 2 4 6 8 10 12 14 16 18 总和:90

如何理解实现Callable接口的方式创建多线程比实现
Runable接口创建多线程方式强大?
1、call()可以有返回值
2、call() 可以跑出异常,被外面的操作铺货,
获取异常的信息
3、Callable是支持泛型的

新增方式二:使用线程池

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

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

|-- 好处:

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

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

3)便于线程管理:

  corePoolSize:核心池的大小

  maximumPoolSIze:最大线程数

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

4)JDK5.0提供了线程池相关API:ExecutorService和Executors

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

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

 --> <T>Futuure<T>submit(Callable<T>task):执行任务,有返回值,一般用来执行Callable

 --> void shutdown():关闭连接池

6)Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

 --> Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池

   --> Executors.newFixedThreadPool(n);:创建一个可重用固定线程数的线程池(常用的一个普通的线程池)

 --> Executors.newSingleThreadPool():创建一个只有一个线程的线程池

 --> Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

package com.csii.day04;

import java.util.concurrent.*;

/**
 * @Author wufq
 * @Date 2020/12/28 16:33
 * 创建线程的方式四:使用线程池
 */
class NumberTest implements Runnable{

    @Override
    public void run() {
        int sum = 0;

        for(int i=0;i<20;i++){
            if(i%2 == 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
                sum += i;
            }
        }

        System.out.println("偶数和:"+sum);

    }
}

class NumberTest2 implements Runnable{

    @Override
    public void run() {
        int sum = 0;


        for(int i=0;i<10;i++){
            if(i%2 == 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
                sum += i;
            }
        }
        
        System.out.println("偶数和:"+sum);
    }
}

class NumberTest1 implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {

        int sum = 0;

        for(int i=0;i<20;i++){
            if(i%2 != 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
                sum += i;
            }
        }

        return sum;
    }
}


public class ThreadPool {

    public static void main(String[] args){

//        1、提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);

        /*
        * 验证ExecutorService接口常用的子类
        * */
        System.out.println(service.getClass());//class java.util.concurrent.ThreadPoolExecutor


//        2、执行指定的线程的操作,需要提供实现Runable接口或Callable接口实现类的对象
        service.execute(new NumberTest());

        service.execute(new NumberTest2());

        FutureTask<Integer> task = new FutureTask<>(new NumberTest1());

        service.submit(task);

        try {
            /*
            * ExecutorService类的两个常用方法:
            * 1、execute用来执行Runable接口 -->没有返回值
            * 2、submit用来执行Callable接口 -->有返回值
            * */
            Integer sum = task.get();
            System.out.println("奇数和:"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

//        3、关闭连接池
        service.shutdown();
    }
}
=====执行结果====

class java.util.concurrent.ThreadPoolExecutor
pool-1-thread-1:0
pool-1-thread-1:2
pool-1-thread-1:4
pool-1-thread-1:6
pool-1-thread-1:8
pool-1-thread-1:10
pool-1-thread-1:12
pool-1-thread-1:14
pool-1-thread-1:16
pool-1-thread-1:18
偶数和:90
pool-1-thread-2:0
pool-1-thread-2:2
pool-1-thread-2:4
pool-1-thread-2:6
pool-1-thread-2:8
偶数和:20
pool-1-thread-3:1
pool-1-thread-3:3
pool-1-thread-3:5
pool-1-thread-3:7
pool-1-thread-3:9
pool-1-thread-3:11
pool-1-thread-3:13
pool-1-thread-3:15
pool-1-thread-3:17
pool-1-thread-3:19
奇数和:100
package com.csii.day04;

import java.util.concurrent.*;

/**
 * @Author wufq
 * @Date 2020/12/28 16:33
 * 创建线程的方式四:使用线程池
 */
class NumberTest implements Runnable{

    @Override
    public void run() {
        int sum = 0;

        for(int i=0;i<20;i++){
            if(i%2 == 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
                sum += i;
            }
        }

        System.out.println("偶数和:"+sum);

    }
}

class NumberTest2 implements Runnable{

    @Override
    public void run() {
        int sum = 0;


        for(int i=0;i<10;i++){
            if(i%2 == 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
                sum += i;
            }
        }

        System.out.println("偶数和:"+sum);
    }
}

class NumberTest1 implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {

        int sum = 0;

        for(int i=0;i<20;i++){
            if(i%2 != 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
                sum += i;
            }
        }

        return sum;
    }
}


public class ThreadPool {

    public static void main(String[] args){

//        1、提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(2);

        /*
        * 验证ExecutorService接口常用的子类
        * */
        System.out.println(service.getClass());//class java.util.concurrent.ThreadPoolExecutor


//        2、执行指定的线程的操作,需要提供实现Runable接口或Callable接口实现类的对象
        service.execute(new NumberTest());

        service.execute(new NumberTest2());

        FutureTask<Integer> task = new FutureTask<>(new NumberTest1());

        service.submit(task);

        try {
            /*
            * ExecutorService类的两个常用方法:
            * 1、execute用来执行Runable接口 -->没有返回值
            * 2、submit用来执行Callable接口 -->有返回值
            * */
            Integer sum = task.get();
            System.out.println("奇数和:"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

//        3、关闭连接池
        service.shutdown();
    }
}
====执行结果====
class java.util.concurrent.ThreadPoolExecutor
pool-1-thread-1:0
pool-1-thread-1:2
pool-1-thread-1:4
pool-1-thread-1:6
pool-1-thread-1:8
pool-1-thread-1:10
pool-1-thread-1:12
pool-1-thread-1:14
pool-1-thread-1:16
pool-1-thread-1:18
偶数和:90
pool-1-thread-2:0
pool-1-thread-2:2
pool-1-thread-2:4
pool-1-thread-2:6
pool-1-thread-2:8
偶数和:20
pool-1-thread-1:1
pool-1-thread-1:3
pool-1-thread-1:5
pool-1-thread-1:7
pool-1-thread-1:9
pool-1-thread-1:11
pool-1-thread-1:13
pool-1-thread-1:15
pool-1-thread-1:17
pool-1-thread-1:19
奇数和:100

通过以上两段代码发现:

第一段代码:设置的线程连接池内的线程数大于实际运行线程数时,线程会按照实际的线程运行

第二段代码:设置的线程连接池内的线程数小于实际运行线程数时,超出的线程会重新按照线程1开始运行,映射到压测时,这也是为什么连接数少的时候,而又并发的线程数大的时候,请求少

导致TPS低的原因

原文地址:https://www.cnblogs.com/frankruby/p/14179235.html