八. IDEA使用和多线程

1. 概述高级知识点(来自尚学堂视频)

  1. 第8章 多线程
  2. 第9章 java常用类
  3. 第10章 枚举类和注解
  4. 第11 章 java集合
  5. 第12章 泛型
  6. 第13章 IO流
  7. 第14章 网络编程
  8. 第15章 java反射机制(难)
  9. 第16章 java8的其他新特性
  10. 第17章 java9&10&11新特性

2. 完美卸载IDEA

两处:①是idea安装目录所在文件夹;

②是c盘安装路径

程序面板——>卸载; 找到idea,右键卸载,全部勾选。卸载后最好重启电脑再重装。

3. IDEA使用

.exe下载自定义安装;.zip解压直接使用

JDK:开发+运行

JRE:运行代码

安装过程:

勾选64位;不勾选所有文件关联;等待安装。

idea修改配置参数,提升运行流畅度:

安装目录下: bin——>vmoptions文件打开:

-Xms128m
-Xmx750m
-XX:ReservedCodeCacheSize=512m
-XX:+UseConcMarkSweepGC
-XX:SoftRefLRUPolicyMSPerMB=50
-XX:CICompilerCount=2
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow
-ea
-Dsun.io.useCanonCaches=false
-Djdk.http.auth.tunneling.disabledSchemes=""
-Djdk.attach.allowAttachSelf=true
-Djdk.module.illegalAccess.silent=true
-Dkotlinx.coroutines.debug=off

①16G内存的电脑可设置为 -Xms512m(设置初始内存数,可提高java程序启动速度)

②同样可设为 -Xmx1500m (设置最大内存数,提高该值,可减少内存Garage收集的频率,提高程序性能)

③-XX:ReservedCodeCacheSize=512m (表示保留代码占用的内存容量)目前不用改了

官方提供的IDEA插件下载地址:https://plugins.jetbrains.com/idea

创建java工程,选择好JDK,选择好所存放目录。

tips:输入要输出的语句.sout回车自动创建好输出代码。

modules的理解和创建

在project工程名右键创建一个模块module,module下再创建功能模块及类等。

但是注意,idea project表示一个项目,module仅仅是模块,不是项目。

4. idea常用设置

进入settings设置界面:

4.1 设置主题

  • 设置窗体及菜单的字体及大小

Appearance & Behavior

4.2 设置编辑区主题

Editor——>Color Scheme 获取更多主题,网站下载:http://www.riaway.com/ 下载好后,导入:file——>import settings——>选中下载的jar文件,一路确认,重启即可。

方式2安装插件。

4.3 鼠标滚轮设置字体大小

settings——>Editor——>General中勾选Change font size with Ctrl+鼠标

4.4 设置鼠标悬浮提示

Editor——>General——>Appearance——>Other勾选show quick documentation on mouse more 后面是反应时间ms

4.5设置自动导包功能

Editor——>General——>Auto import——>两个都勾选,选择Always

4.6 设置显示行号和方法间的分隔符

Editor——>General——>Appearance——>show line numbers + show method separators都勾选

4.7 忽略大小写提示

idea默认区分大小写,比如输入stringBuffer,idea默认不会提示和进行代码补充;除非我们输入StringBuffer就可以进行提示和补充。不想区分大小写,改为none即可。

Editor——>General——>Code Completion, 选择none。或者去掉勾选Match case

4.8 设置取消单行显示tabs的操作

打开很多文件时,idea默认把所有打开的文件名tab单行显示,单行会隐藏超过界面部分的tab,效率不高,找文件不方便。

Editor——>General——>Editor Tabs 取消选中show tabs in one row

4.9 设置默认字体、字体大小、字体行间距

Editor——>Font 里,font、size、line spacing自定义。

4.10 修改编辑区当前主题的字体、字体大小及行间距(忽略)

Editor——>Color Scheme——>

4.11 设置控制台输出字体及大小

Editor——>Color Scheme——>Console Font

4.12 修改代码中注释的字体颜色

Editor——>Color Scheme——>Language Defaults——>Comments里自定义。

4.13 设置超过指定import个数,改为*

Editor——>CodeStyle——>Java——>横栏里点Imports,最下方两个框输入个数自定义。

4.14 修改类头的文档注释信息

Editor——>File and Code Templates

4.15 设置项目文件编码

Editor——>File Encodings 选择utf-8

4.16 设置当前源文件的编码

可以在编辑区右下角设置编码

4.17 设置自动编译

Build,Execution,Deployment(构建,执行,部署)——>Compiler右边两项,build project automatically和compile independent modules in parallel选中打勾,即源文件修改后自动编译。

4.18 设置省电模式

顶部栏file——>power save mode 勾选后就没有代码提示等操作。不要误选。

4.19 两个代码文件垂直/水平显示

右键点击编辑区某一个java文件名,选择split right或者split down分别表示垂直左右显示和水平上下显示。

5. idea快捷键设置

Settings——>Keymap,快捷键设置,右侧搜索框输入要实现的功能,显示相关快捷键。搜索框右侧放大镜输入快捷键显示执行什么功能。

也可以导入其他编辑器的快捷键。在上方选择。

常用快捷键

单行注释: Ctrl + /

多行注释: Ctrl + Shift + /

向下移动行(显示被界面遮挡行):Alt + ↓

向上移动行(显示被界面遮挡行):Alt + ↑

提示补全: Alt + /

复制这行到下一行: Ctrl + D

复制Ctrl + C

粘贴 Ctrl + V

剪切 Ctrl + X

保存 Ctrl + S

撤销 Ctrl + Z

反撤销 Ctrl + Y

全选 Ctrl + A

选中数行,整体向后移动 tab

选中数行,整体向前移动 shift + tab

查看类的继承关系: Ctrl + H

查找 : Ctrl + F

查找文件: 双击shift

添加try/catch或者其他语句块(Surround with):Alt + Shift + Enter !!!

等等。

6. idea模板的使用

① Editor——>General——>Postfix Completion只能用,固定的

代码缩写自动调用模板补全

如sout 5.for 等等

soutp回车输出形参值

②Editor——>Live Templates很多模板 ,可以修改或自己添加

如main方法 psvm

----------多线程-----------

7. 程序进程线程的概念

  • 程序:未完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码(没有运行起来),静态对象。

  • 进程:是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程。有它自身的生命周期

  • 线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。

    • 一个进程并行执行多个线程,则是支持多线程的;
    • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销较进程小
    • 同一进程内的多个线程共享相同的内存单元/内存地址空间——>他们从同一堆中分配对象,可以访问相同的变量和对象。使线程间通信更简便高效。但是,多个线程共享的系统资源可能会带来安全隐患(如资源竞争)。

在这里插入图片描述

8. 单、多核CPU理解及任务的并行、并发

  • 单核CPU,其实是一种假的多线程,因为一个时间单元内只能执行一个线程。比如虽然多车道,但收费站只有一个工作人员在收费(CPU);多核才能更好发挥多线程效率。
  • 一个Java应用程序至少有3个线程:main()主线程,gc()垃圾回收线程,异常处理线程。如果发生异常,会影响主线程。
  • 并行与并发

并行:多个CPU同时执行多个任务。比如:过个人同时做不同的事。

并发:一个CPU(采用时间片)同时执行多个任务,比如:,秒杀、多个人做一件事。

9. 多线程的优点

单核CPU情况下,同时发送两个文件比发完一个再发一个要慢。(因为单核看似同时,其实是切换来执行)。

多核下,同时发送更快。

  1. 提高程序的响应。,对图形化界面操作更有意义,影响用户体验。
  2. 提高计算机CPU利用率(不用等待时间片)
  3. 改善程序结构。将复杂的进程分为多个线程,便于理解和修改。

何时需要多线程?

  • 程序需要同时执行两个或多个任务
  • 程序需要实现一些需要等待的任务时,如用户输入、文件读写、网络操作、搜索等。
  • 需要一些后台运行的程序时。

10. 创建多线程方式一:继承Thread类

一条线执行下来的是单线程。

  • Java的jvm允许程序运行多个线程,通过java.lang.Thread类来体现。

  • Thread类的特性

①每个线程都是通过某个特定的Thread对象的run()方法来完成操作的,经常把run()方法的主体成为线程体

②通过该Thread对象的start()方法来启动这个线程,而非直接调用run()。

//多线程的创建方式一:继承Thread类
/*
* 1. 创建一个继承于Thread类的子类
* 2. 重写Thread类的run()
* 3. 创建Thread类的子类的对象
* 4. 通过此对象调用start()
* */

//遍历100以内的偶数
//1. 创建一个继承于Thread类的子类
class MyThread  extends Thread {
    //2. 重写Thread类的run()
    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0){
                System.out.println(i);
            }
        }
    }
}

public class ThreadTest01{
    public static void main(String[] args) {
        //3. 创建Thread类的子类的对象
        MyThread t1 = new MyThread();
        //4. 通过此对象调用start()
        t1.start();
        
        for (int i = 0; i < 100; i++) {
            if (i % 2 != 0){
                System.out.println(i);
            }
        }
    }
}

分析:t1.start()执行到这里还是主线程执行的;之后run方法自动执行是新线程执行的;下面的for循环还是主线程执行的。主线程执行得比较快就会先输出奇数;再输出偶数。

11. 创建多线程两个问题的说明

start()方法的作用:①启动当前线程②调用当前线程的run()方法

本来要执行Thread类的run()方法,但同时子类重写了run()方法,所以调用了子类的run()方法。

问题1:能否直接使用t1.run()开启新的线程,不适用start()???

答:显然不能。java规定start()方法启动线程,直接调用run()方法只是在执行该方法,并不是多线程。

获取当前线程名:Thread.currentThread().getName()

问题2:再启动一个线程,遍历执行100以内的偶数?

答:会报异常。因为start()方法只能去调用一次。这时候需要重新新建一个线程对象t2,来调用start()方法。

12. 线程的常用方法

Thread类的有关方法:

  1. void start() 启动线程并执行的对象的run()方法
  2. run() 线程在被调度时执行的操作,常需要重写该方法
  3. String getName(): 返回线程的名称
  4. void setName(String name): 设置该线程名称
  5. static Thread currentThread(): 返回当前线程。
  6. yield(): 释放当前CPU的执行权
  7. join(): 阻塞当前线程,先完成join进来的方法再恢复之前的执行。
  8. stop(): 强制结束线程,已过时。
  9. sleep(long milltime): 阻塞,线程睡眠一段时间,单位毫秒
  10. isAlive(): 判断当前线程是否还存活
/*
测试Thread类常用方法
 */
class Thread01 extends Thread{
    @Override
    public void run(){
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

public class ThreadMethodTest {
    public static void main(String[] args) {
        Thread01 t1 = new Thread01();

        // 给线程起名
        t1.setName("线程1");
        t1.start();
        //给主线程命名
        Thread.currentThread().setName("main主线程");
        System.out.println(Thread.currentThread().getName() + ":" + "哈哈哈");
    }
}

给线程起名方式一:t1.setName(“线程1”);

给线程起名方式二:

//在线程子类中通过构造器重载给线程命名
    public Thread01(String name){
        super(name);
    }
//main主方法里new新的thread对象时将名字作为参数传进去。
Thread01 t1 = new Thread01("thread000");

13. 线程的调度(优先级)

  • 调度策略

时间片切换方式

抢占式:高优先级线程抢占CPU

  • Java的调度方法

同优先级线程组成先进先出队列,先到先服务,使用时间片策略。

对高优先级,使用优先调度的抢占式策略。

  • 线程的优先级(3个常量):

MAX_PRIORITY:10

MIN_PRIORITY:1

NORM_PRIORITY:5 线程默认的优先级

  • 涉及的方法

getPriority():返回线程优先值

setPriority():改变线程的优先级

  • 说明

线程创建时继承父线程的优先级

低优先级只是获得调度的概率低,并非一定排在高优先级线程之后才被调用

14. 例题:继承Thread方式,多窗口卖票

/*
例子:创建三个窗口卖票,总票数为100张
存在线程安全问题,待解决。
 */
class Window extends Thread{

    private static int ticket = 100; //声明为静态,只有一份共用

    @Override
    public void run(){
        while (true){
            if (ticket > 0){
                System.out.println(getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else {
                break;
            }
        }
    }
}

public class WindowTest {
    public static void main(String[] args) {
        Window t1 = new Window();
        Window t2 = new Window();
        Window t3 = new Window();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

15. 创建多线程方式二:实现Runnable接口

/*
创建多线程方式二:实现runnable接口
1. 创建一个实现了Runnable接口的类
2. 实现类去实现Runnable中的抽象方法:run()
3. 创建实现类的对象
4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5. 通过Thread类的对象调用tart()s
 */
//1. 创建一个实现了Runnable接口的类
class MThread implements Runnable{
    //2. 实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

public class RunnableImplTest {
    public static void main(String[] args) {
        //3. 创建实现类的对象
        MThread t1 = new MThread();
        //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread tt1 = new Thread(t1);
        //5. 通过Thread类的对象调用start()
        tt1.setName("线程1");
        tt1.start();  //为什么调用当前线程的run却执行了重写的run?
        //因为当前线程run里面调用了runnable类型的target的run()方法,此时的target就是传进来的参数t1

        Thread tt2 = new Thread(t1);
        tt2.setName("线程2");
        tt2.start();
    }
}

16. 例题:Runnable方式实现多窗口卖票

/*
例子:创建三个窗口卖票,总票数为100张,使用Runnable接口的方式
存在线程安全问题,待解决。
 */
class Window1 implements Runnable{

    private int ticket = 100;  //这里没加static,仍然是同一个ticket,原因只new了一个对象t1作为参数传进去。

    @Override
    public void run(){
        while (true) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);
                ticket--;
            } else {
                break;
            }
        }
    }
}


public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();
        //三者始终使用同一对象
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

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

17. 两种创建方式的对比

接口实现更推荐;继承有局限性,无法再继承其他类

另外接口实现方式不用将变量声明为static;

  • 开发中优先选择,实现Runnable接口的方式
  • 原因①实现的方式没有类的单继承的局限性
  • 原因②实现的方式更适合处理多线程有共享数据的情况

其实,源码中,Thread类本身实现了Runnable接口;两种方式都需要将线程要执行的逻辑重写在run()方法。

18. 线程的生命周期

JDK中用Thread.State类定义了线程的几种状态。

一个完整的生命周期经历的几种状态:

  • 新建:当一个Thread类或其子类对象被声明并创建时,此时线程处于新建状态; new
  • 就绪:当新建的线程被start()之后,将进入线程队列等待CPU时间片,此时已经具备了运行的条件,只是没分配到CPU资源 start()
  • 运行:当就绪的线程被调度并获得CUP资源时,便进入运行状态,run()方法定义了线程的操作和功能 run()
  • 阻塞:在某种特殊情况下,被认为挂起或执行输入输出操作时,让出CPU并临时终止了自己的执行,进入阻塞状态 如sleep() join() wait() 等待同步锁 suspend()
  • 死亡:线程完成了它的全部工作或线程被提前强制中止或出现异常导致线程结束 stop()或执行完run()或异常

在这里插入图片描述

19. 理解线程的安全问题

问题:

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

案例:

①实现Runnable接口的方式

三个窗口卖票,总共100张, 会出现三个100。出现重票;

当ticket > 0时加sleep让线程睡一段时间,再输出,此时出现每个窗口都能卖100张票/错票;

等等。

问题的原因?

当某个线程操作车票过程中,操作尚未完成时,其他线程参与进来,也操作车票。

如何解决?

当一个线程a在操作票时,其他线程不能参与进来,直到该线程a操作完后,其他线程才可以开始操作ticket,即使a出现了阻塞,也不能改变。

20. 同步代码块处理Runnable方式线程安全问题

在Java中,通过同步机制解决线程安全问题。

方式一:同步代码块

关键字:

synchronized(同步监视器){
	//需要被同步的代码
}

tips:操作共享数据的代码,即需要被同步的代码。

共享数据:多个线程共同操作的变量,如卖票中的ticket

同步监视器:俗称 锁。任何一个类的对象都可以充当锁。

补充:可以使用this替代new的类对象(前提this是唯一)

/*
例子:创建三个窗口卖票,总票数为100张,使用Runnable接口的方式
存在线程安全问题,待解决。
使用synchronized关键字
1. 包住涉及到共享数据的代码块  包的不能多也不能少。
2, new一个对象充当锁
3. 要求:多个线程共用同一把锁,所以锁(对象)的声明位置很重要!!!(匿名类对象也不行)
4. 同步方式好处:解决线程安全问题;操作同步代码时,只能有一个线程参与,其他线程等待,相当于单线程的过程,效率低。-- 缺点。
 */
class Window1 implements Runnable{

    private int ticket = 100;  
    Object obj = new Object(); // 2. 这里随机实例化一个类对象充当下面的锁
	//可以不new新的对象,直接用this
    @Override
    public void run(){
        while (true) {   //此处可替换为this
            synchronized (obj) {  //1. 下面都是需要被同步的代码
                if (ticket > 0) {

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

                    System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();
        //三者始终使用同一对象
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

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

21. 同步代码块处理继承Thread类方式线程安全问题

sleep一段时间,同样出现重票问题。

使用synchronized关键字。如上篇方式解决,结果仍然有问题。原因:new了三个对象,obj对象不唯一。

解决:将obj声明为static类型。

补充:这里慎用this充当obj,(除非this唯一),可以用当前类名.class替代

/*
例子:创建三个窗口卖票,总票数为100张,使用继承Thread类的方式
存在线程安全问题,待解决。
同样synchronized关键字
 */
class Window extends Thread{

    private static int ticket = 100; //声明为静态,只有一份共用

    private static Object obj = new Object(); //obj对象声明为static类型!

    @Override
    public void run(){
        while (true){//此处不能用this替代obj,因为有三个对象。但是可以有另一种替代,即window.class(类名.class)(拿当前类充当,证明了类也是对象!),避免new对象
            synchronized (obj) {
                if (ticket > 0) {

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

                    System.out.println(getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

public class WindowTest {
    public static void main(String[] args) {
        Window t1 = new Window(); //声明了三个类对象
        Window t2 = new Window();
        Window t3 = new Window();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

改进:有没有相对简单的对象,不用new一个对象的方式?

当前对象。this。即当前类的对象。但是Thread类方式里不能用这个,因为有三个对象。

那么有一个替代方式,即当前类名.class替代obj。

22. 同步方法处理实现Runnable方式线程安全问题

即:如果操作共享数据的代码完整的生命在一个方法中,我们就将该方法声明为同步的。

同步方法:即在该方法返回类型前加synchronized关键字。

/*
使用同步方法解决实现Runnable接口的线程安全问题
 */
class Window2 implements Runnable{

    private int ticket = 100;  //这里没加static,仍然是同一个ticket,原因只new了一个对象w2作为参数传进去。
    private boolean flag = true;
    @Override
    public void run(){
        while (flag) {
            show();
        }
    }

    private synchronized void show(){
        if (ticket > 0) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);
            ticket--;
        }else flag = false;
    }
}

public class WindowTest2 {
    public static void main(String[] args) {
        Window2 w2 = new Window2();
        //三者始终使用同一对象
        Thread t1 = new Thread(w2);
        Thread t2 = new Thread(w2);
        Thread t3 = new Thread(w2);

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

23. 同步方法处理继承Thread类方式线程安全问题

/*
例子:创建三个窗口卖票,总票数为100张,使用继承Thread类的方式
存在线程安全问题,待解决。
使用同步方法来解决继承Thread类方式的线程安全问题
1. 同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
2. 非静态的同步方法,同步监视器是:this
    静态的同步方法,同步监视器是:当前类本身。
 */
class Window3 extends Thread{

    private static int ticket = 100; //声明为静态,只有一份共用

    @Override
    public void run(){
        while (true){
            show();
        }
    }
    private static synchronized void show(){  //写为static类型,此时同步监视器唯一,但不是this,是当前类Window3,当前类唯一
    //private synchronized void show(){ //此时同步监视器t1,t2,t3不安全
        if (ticket > 0) {

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

            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

public class WindowTest3 {
    public static void main(String[] args) {
        Window3 t1 = new Window3();
        Window3 t2 = new Window3();
        Window3 t3 = new Window3();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

24. 线程安全的单例模式之懒汉式

懒汉式是不安全的;饿汉式是安全的。

懒汉式改进。

/*
使用同步机制将单例模式中的懒汉式改写为线程安全的。
加synchronized,静态方法同步锁是类本身
 */
public class BankTest {
}

class Bank{
    private Bank(){}

    private static Bank instance = null;

//    public static synchronized Bank getInstance(){
//        if (instance == null){
//            instance = new Bank();
//        }
//        return instance;
//    }//与下面雷同,只是同步方法和同步代码块的区别
    public static Bank getInstance() {
        //方式一:效率不高,多个线程只有第一个进去初始化后,后面每个线程还需要继续排队返回instance
//        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;
    }
}

25. 多线程死锁的问题

  • 死锁
    • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
    • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态后,无法继续
  • 解决方法
    • 专门的算法、原则
    • 尽量减少同步资源的定义
    • 尽量避免嵌套同步
/*
演示线程的死锁问题
 */
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.append("a");
                    s2.append("1");

                    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");

                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");

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

//输出   这种会出现死锁,只是概率小一些。另外输出结果不唯一,也可能限制性下面的结果为cd 34 cdab 3412
ab
12
abcd
1234

增加死锁的概率:

sleep 100

/*
演示线程的死锁问题
1. 死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,
都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
2. 说明:死锁出现,无异常无报错,阻塞,无法继续。
 */
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.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();
    }
}
//此时运行后既不报错也不输出任何东西,卡住一直在运行

例2:

class A{
    public synchronized void foo(C b) {
        System.out.println("当前线程名:" + Thread.currentThread().getName()
                 + "进入了A实例的foo方法");
        try {
            Thread.sleep(100);
        } catch (InterruptedException ex){
            ex.printStackTrace();
        }
        System.out.println("当前线程名:" + Thread.currentThread().getName()
                + "企图调用C实例的last方法");
        b.last();
    }

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

class C{
    public synchronized void bar(A a) {
        System.out.println("当前线程名:" + Thread.currentThread().getName()
                + "进入了C实例的bar方法");
        try {
            Thread.sleep(200);
        } catch (InterruptedException ex){
            ex.printStackTrace();
        }
        System.out.println("当前线程名:" + Thread.currentThread().getName()
                + "企图调用A实例的last方法");
        a.last();
    }

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

public class ThreadTest2 implements Runnable{
    A a = new A();
    C b = new C();

    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) {
        ThreadTest2 d1 = new ThreadTest2();
        new Thread(d1).start();  //调分线程run方法

        d1.init();  //调主线程init方法
    }
}

26. Lock锁方式解决线程安全问题

  • 从JDK5开始,java提供了——通过显示定义同步锁对象来实现同步,同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lovk对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。
/*
解决线程安全问题的方式三:Lock锁 -----JDK5定义
 */
class Window4 implements Runnable{
    private int ticket = 100;
    //①实例化一个lock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run(){
        while (true){
            try {
                //2. 调用加锁方法Lock()
                lock.lock();

                if (ticket > 0){

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

                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                lock.unlock(); //3。调用解锁的方法
            }

        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window4 w = new Window4();

        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和lock的异同

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

不同: synchronized执行完响应逻辑后自动释放同步监视器;lock需要手动启动同步(lock())和结束同步(unlock())。

27. 同步机制的课后练习

存在线程安全问题的代码:

/*
题目:银行有一个账户。
有两个储户分别向该账户存3000元,每次存1000,存3次。每次存完打印该账户余额。

分析:
1. 是否为多线程问题?  是,两个储户线程
2. 是否有共享数据?  是,账户
3. 是否有线程安全问题?  是
4. 如何解决?   同步机制3种方式  Thread、runnable、lock

 */

class Account{

    private double balance;

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

    public void storeMoney(double money){
        if (money > 0){
            balance += money;

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


            System.out.println(Thread.currentThread().getName() + ": 存钱成功, 余额为:" + balance);
        }
    }
}

class Custom extends Thread{

    private Account acct;

    public Custom(Account acct) {
        this.acct = acct;
    }

    @Override
    public void run(){
        for (int i = 0; i < 3; i++) {
            acct.storeMoney(1000);
        }
    }
}

public class AccountTest {
    public static void main(String[] args) {
        Account acct = new Account(0);
        Custom c1 = new Custom(acct);//这里this指账户,共用一个,所以可用
        Custom c2 = new Custom(acct);

        c1.setName("甲");
        c2.setName("乙");

        c1.start();
        c2.start();
    }
}
//: 存钱成功, 余额为:2000.0: 存钱成功, 余额为:2000.0: 存钱成功, 余额为:4000.0: 存钱成功, 余额为:4000.0: 存钱成功, 余额为:6000.0: 存钱成功, 余额为:6000.0

解决: 给存钱方法加一个synchronized即可。

输出:

甲: 存钱成功, 余额为:1000.0
甲: 存钱成功, 余额为:2000.0
甲: 存钱成功, 余额为:3000.0
乙: 存钱成功, 余额为:4000.0
乙: 存钱成功, 余额为:5000.0
乙: 存钱成功, 余额为:6000.0

28. 线程的通信

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

涉及到的三个方法:

wait():执行此方法,当前线程进入阻塞状态,并释放同步监视器

notify():执行此方法,唤醒被wait的一个线程;若有多个,唤醒优先级较高的一个。

notifyAll():执行此方法,唤醒所有被wait的方法

tips:

  1. wait、notify、notifyAll只能用于同步方法或同步代码块当中。

  2. 都是this调用的这些方法;即调用他们的对象必须是同步代码块或同步方法中的同步监视器。否则会出现Illegal异常。

  3. 这三个方法都是定义在java.lang.Object类中。

/*
线程通信的例子
使用两个线程打印1-100,线程1线程2交替打印
 */
class Number implements Runnable{
    private int number = 1;
    @Override
    public void run(){
        while (true){
            synchronized (this) {

                notify(); // 唤醒线程    2进来后唤醒了1但此时是2拿着锁,1进不来

                if (number <= 100) {

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

                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    try {
                        wait();  //使得调用该方法的线程进入阻塞状态  2到这里释放了锁,然后1拿着进来(sleep不会释放锁)
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                } else {
                    break;
                }
            }
        }
    }
}


public class ThreadPrint {
    public static void main(String[] args) {
        Number num = new Number();
        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
...

29. 面试题及总结

  1. sleep和wait方法的异同

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

不同:①两方法声明的位置不同:Thread类中声明sleep()。Object类中声明wait();

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

③关于是否释放同步监视器:若两个方法都用在同步代码块或同步方法中,sleep不释放;wait释放。

30. 经典例题:生产者/消费者问题

/*
线程通信的应用:经典例题:生产者消费者问题
分析:属于多线程问题,生产者线程,消费者线程
共享数据:店员(或产品)
如何解决:同步机制,三种方法
涉及到线程通信
 */
class Clerk{

    private int count = 0;
    //生产产品
    public synchronized void produceProduct() {

        if (count < 20){
            count++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + count + "个产品");
            notify();//唤醒消费者消费
        }else{
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //消费产品
    public synchronized void consumeProduct() {
        if (count > 0){
            System.out.println(Thread.currentThread().getName() + ":开始消费第" + count + "个产品");
            count--;
            notify(); //唤醒生产者继续生产
        }else{
           //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

class Producer extends Thread{ // 生产者

    private Clerk clerk;

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

    @Override
    public void run(){
        System.out.println(getName() + ":开始生产产品...");
        while (true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceProduct();
        }
    }
}

class Consumer extends Thread{ //消费者

    private Clerk clerk;

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

    @Override
    public void run(){
        System.out.println(getName() + ":开始消费产品...");
        while (true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumeProduct();
        }
    }
}

public class ProductTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Producer p1 = new Producer(clerk);
        p1.setName("生产者1");
        Consumer c1 = new Consumer(clerk);
        c1.setName("消费者1");

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

31. JDK5.0新增线程创建方式三

  • 方式三:实现Callable接口,该接口比Runnable功能更强大

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

    • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
    • FutureTask是Future接口的唯一的实现类
    • FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
/*
创建线程方式三:实现callable接口。  -----jdk5.0新增
步骤:
1. 创建一个实现callable的实现类
2. 实现call方法,将此线程需要执行的操作声明在此方法中
3. 创建Callable接口实现类的对象
4. 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
6. 获取Callable中call()的返回值 (第6步可不要,除非需要将结果给另一个线程)

如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式要强大???
1. call()方法可以有返回值,给另外一个线程(get())
2. call()方法可以抛出异常,被外面操作捕获,获取异常信息
3. Callable是支持泛型的

 */
//1. 创建一个实现callable的实现类
class NumThread implements Callable{
    //2. 实现call方法,将此线程需要执行的操作声明在此方法中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        //遍历100以内的偶数,返回所有偶数的和
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;   //这里涉及到装箱,int不是Object的子类,这里相当于将int转为integer(是object的子类)
    }
}

public class ThreadCallableTest {
    public static void main(String[] args) {
        //3. 创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4. 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();  //启动线程开始执行call()方法

        try {
            //6. 获取Callable中call()的返回值
            //get方法返回值即为futureTask构造器参数callable实现类重写的call()的返回值
            Object sum = futureTask.get(); //目的只是为了获取call()的返回值
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

32. 线程创建方式四:线程池

方式四:使用线程池

开发中真正用的都是线程池。

1.经常创建销毁、使用量特别大的资源,如并发时,对性能影响很大。

2.思路就是,提前创建好多个线程,放入线程池中,使用时直接获取,用完返回池中。可避免频繁创建销毁,实现重复利用。

3.好处:

  • 提高响应速度(减少了创建线程的时间)
  • 降低资源消耗(重复利用线程池中线程,无需每次创建)
  • 便于线程管理(不造成拥堵,如公交车半小时发一次)
    • corePoolSize:核心池的大小
    • maxmumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

线程池相关API

  • jdk5.0开始提供了线程池相关API:ExecutorService 和 Executors
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    1. void execute(Runnable command):执行任务/命令,没有返回值。一般用来执行Runnable
    2. Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
    3. void shutdown():关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
    1. Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    2. Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
    3. Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
    4. Executors.newScheduledThreasPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
/*
创建线程方式四:线程池
 */
class NumberThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; 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 = 0; 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);//接口实现类的对象
        //System.out.println(service.getClass()); //得到具体实现了哪一个类
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service; //强转

        //4. 设置线程池的属性 便于线程管理
       //service1.setCorePoolSize(15);
       // ...

        //2. 执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());   //适合用于Runnable
        service.execute(new NumberThread1());
        //service.submit();   //适合用于Callable
        //3. 关闭连接池
        service.shutdown();
    }
}
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值