多线程详解(二)

4.4 题外话

根据多线程详解(一)的同步,
我们可以使用同步机制将单例模式中的懒汉式改写为线程安全的。

举例:

public class BankTest {  }
class Bank{
    private Bank(){}
    private static Bank instance = null;

    public static Bank getInstance() {
//方式一:在方法上 加 synchronized 效率稍差

//方式二:效率稍差 只要判断一次同步一次 后面就不用同步了
//解决方案就相当于只有一台手机 买完人走了 出公告 手机卖完了 不用再进来了
//        synchronized (Bank.class) {
//            if (instance == null){
//                instance = new Bank();
//            }
//            return instance;
//        }

        //方式三:效率更高
        if (instance == null){
            synchronized (Bank.class) {
                if (instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance;//这行就不算操作
    }
}

5 线程的死锁问题

5.1 概念

5.1.1 死锁的理解

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

5.1.2 说明

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

2.我们使用同步时,要避免出现死锁

5.1.3举例

一人一个筷子,互不相让,就打起来了。

5.2 解决方法

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

5.3 演示线程的死锁问题

public class ThreadTest {
    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();
        new Thread(){
            @Override
            public void run() {
                synchronized (s1){//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();
    }
}

一个等着拿s2 一个拿着s2等着拿s1 互相僵持

6.JDK5.0 新增解决线程安全问题

6.1 概念

解决线程安全问题的方式三: Lock锁 —JDK5.0新增

》通过显式定义同步锁对象来实现同步。
》同步锁使用Lock对象充当。

6.2 步骤

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

6.3 举例

使用Lock锁解决实现Runnable接口的线程安全问题

import java.util.concurrent.locks.ReentrantLock;

class Window implements Runnable{
    private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();
    //默认值是false  参数是boolean fair(公平)
    // 设置true 先进先出 一个个执行 不会出现有个执行了下一刻又抢到了
    @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 LockTest {
    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();
    }
}

如果你用的是继承于Thread类 lock要加个静态 要用同一个

6.4 synchronized与Lock 的异同?(面试题)

相同:
二者都可以解决线程安全问题

不同:
synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。
Lock需要手动的启动同步(Lock()) ,同时结束同步也需要手动的实现(unLock())。

优先使用顺序:
Lock >同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)

7.线程的通信

7.1 线程通信的例子

使用两个线程打印1-100。线程1,线程2交替打印(一个一个交互进入)

7.2 涉及到三个方法及注意点

7.2.1 方法

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

7.2.2 注意点:

1.wait(), notify(), notifyAll() 三个方法必须使用在同步代码块或同步方法中。

2.wait(), notify(), notifyAll() 三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则,会出现IllegaLMonitorStateException异常

3.wait(), notify(), notifyAll() 三个方法是定义在java.lang.Object类中。

7.2.3 面试题: sleep() 和 wait() 的异同?

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

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

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

3)关于是否释放同步监视器:
如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait() 会释放锁。

7.3 举例

class Number implements Runnable {
    private int number = 1;//共享数据

    @Override
    public void run() {
        while (true){
            synchronized (this) { //this代表number对象
                notify();//线程一把线程二唤醒 线程二把线程一唤醒 notifyAll()就是唤醒所有  省略了this
                if (number <= 100) {
                    try {
                        Thread.sleep(10);//sleep 不会释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                    //使得调用如下wait() 方法的线程进入阻塞状态 wait会释放锁
                    try {
                        wait();//  省略了this
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else {
                    break;
                }
            }
        }
    }
}
public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}

8.JDK5.0新增线程创建方式

8.1 多线程创建,方式三:实现Callable接口

8.1.1 步骤

Future接口最重要

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

8.1.2 Callable比Runnable更强大

如何理解实现Callable接口的方式创建多线程
比实现Runnable接口创建多线程方式要强大

  1. call()可以有返回值的
  2. call() 可以抛出异常,被外面的操作捕获,获取异常的信息
  3. Callable是支持泛型的

8.1.3 举例

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

//1.创建一个实现Callable的实现类
class NumThread implements Callable<Integer> {
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Integer 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 ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask 的对象
        FutureTask<Integer> futureTask = new FutureTask<>(numThread);
        //5.将Future Task的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();
        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的calL()的返回值。
            Integer sum = futureTask.get();//调get方法获取Callable接口实现类的回调方法
            System.out.println("总和为:" + sum);//主线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

8.2 多线程创建,方式四:使用线程池(开发常用的)

8.2.1 介绍

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

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

8.2.2 步骤

  1. 提供指定线程数量的线程池
  2. 执行指定的线程的操作。
    需要提供实现Runnable接口 或 Callable接口实现类的对象
  3. 关闭线程池

8.2.3 好处

  1. 提高响应速度(减少了创建新线程的时间)
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  3. 便于线程管理
    corePoolSize:核心池的大小
    maximumPoolSize:最大线程数
    keepAliveTime:线程没有任务时最多保持多长时间后会终止

8.2.4 举例

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

class NumberThread implements Runnable {
    @Override
    public void run() {
        for (int i = 1 ; i <= 100 ; i++) {
            if (i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);//分线程
            }
        }
    }
}
class NumberThread1 implements Runnable {
    @Override
    public void run() {
        for (int i = 1 ; i <= 100 ; i++) {
            if (i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);//分线程
            }
        }
    }
}
public class ThreadPool {
    public static void main(String[] args) {
        //1.提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);//造了个线程池
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
       //设置线程池的属性
//        System.out.println(service.getClass());//获取是哪个类造的
        service1.setCorePoolSize(15);

        //2.执行指定的线程的操作。需要提供实现Runnable接口 或 Callable接口实现类的对象
        //线程要干什么不知道 所以还是要提供实现 Runnable接口的 实现类
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable
//       service.submit(Callable callable);//适合适用于Callable

        service.shutdown();//3.关闭线程池
    }
}

9. 面试题

一共有几种多线程创建方式? 》 4种

解决线程安全问题? 》3种

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>