java学习|Java高级学习笔记-1(多线程)

多线程 程序,进程,多线程

  • 程序***(program)***是为完成特定任务、用某种语言编写的一组指令的集合。即指一
    段静态的代码,静态对象。
  • 进程***(process)***是程序的一次执行过程,或是正在运行的一个程序。是一个动态
    的过程:有它自身的产生、存在和消亡的过程。——生命周期
    1. 如:运行中的QQ,运行中的MP3播放器
    2. 程序是静态的,进程是动态的
    3. 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
  • 线程*(thread)**,进程可进一步细化为线程,是一个程序内部的一条执行路径。
    1. 若一个进程同一时间并行执行多个线程,就是支持多线程的
    2. 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开
    销小
    1. 一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以
    访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资
    源可能就会带来安全的隐患。
使用多线程的优点
  • **背景:**以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方
    法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
    多线程程序的优点:
    1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
    2. 提高计算机系统CPU的利用率
    3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和
    修改
使用多线程的例子
  • 程序需要同时执行两个或多个任务。
  • 程序需要实现一些需要等待的任务时,如用户输入、文件读写
    操作、网络操作、搜索等。
  • 需要一些后台运行的程序时。
  • 现实例子:
    1. 在杀毒软件里,我们可以病毒检测,病毒查杀,垃圾回收,这几个功能由多个线程完成
    2. 在手机软件里,当我们往下滑动时,图片会一张张的显示,这里都是一个又一个的线程帮助加载的。
线程的创建和使用 获取一个线程
package com.hyb.thread; /** * @program: CreateThread线程的创建 * @description: * 方式一:继承Thread * 1.重写run(); * 2.main()调用start() * @author: Huang Yubin * @create: 2021-06-05 16:30 **/class ThreadTest1 extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2==0){ System.out.println(i); } } } }public class CreateThread { public static void main(String[] args) { ThreadTest1 threadTest1 = new ThreadTest1(); //start作用使得线程开始执行,调用run,一定要调用start,不然就不叫线程了 threadTest1.start(); //调用start这个方法以后,就是一个线程问题了, //然后咋一看,后面这条输出语句一定是最后输出吗? //有了线程就不一定,因为start开始了另一条线程了,这个main线程中 //有两个同时运作的线程,一个上面的,一个是下面的, //输出顺序要看cpu System.out.println("-----------------------"); } }

启动多个线程
  • 上面我们启动了一个线程,那么怎样启动多个线程呢?当然不能让同一对象调用两次start,所以new一个新的对象
package com.hyb.thread; import org.junit.Test; /** * @program: ThreadTest2 * @description: * 创建两个线程 *不同线程执行不同方法 * @author: Huang Yubin * @create: 2021-06-05 17:10 **/class myThread1 extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2==0){ System.out.println(i); } } } }class myThread2 extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { if (i%2!=0){ System.out.println(i); } } } }public class ThreadTest2 { public static void main(String[] args) { myThread1 mythread1 = new myThread1(); mythread1.start(); myThread2 mythread2=new myThread2(); mythread2.start(); //除了上面的方法,还可以采用匿名类调用 //new myThread1(){ //@Override //public void run() { //for (int i = 0; i < 100; i++) { //if (i%2!=0){ //System.out.println(i); //} //} //} //}.start(); // //new myThread2(){ //@Override //public void run() { //for (int i = 0; i < 100; i++) { //if(i%2==0){ //System.out.println(i); //} //} //} //}.start(); } }

在Thread类中的常用方法
  • void start(): 启动线程,并执行对象的run()方法
  • run(): 线程在被调度时执行的操作
  • String getName(): 返回线程的名称
  • void setName(String name):设置该线程名称
  • static Thread currentThread(): 返回当前线程。在Thread子类中就
    是this,通常用于主线程和Runnable实现类
package com.hyb.thread; /** * @program: ThreadMethod * @description: * - **void start():** 启动线程,并执行对象的run()方法 * * - **run():** 线程在被调度时执行的操作 * * - **String getName():** 返回线程的名称 * * - **void setName(String name)**:设置该线程名称 * * - **static Thread currentThread():** 返回当前线程。在Thread子类中就 *是this,通常用于主线程和Runnable实现类 * - **yield() 释放当前线程的执行权,但之后的执行权可能被其他线程执行,或者重新被自己执行 * - **a.join() 在线程b中加入线程a,此刻线程b进入阻塞状态,一旦加入的线程执行完毕,线程b才开始执行 * - **stop()让当前线程终止,不推荐使用 * - **sleep()让线程休眠,1000等于一秒 * - **isAlive()判断当前线程是否存活 * @author: Huang Yubin * @create: 2021-06-05 17:30 **/class ThreadMethodTest extends Thread{ @Override public void run() { //获得当前线程名字 //下面一定得捕获异常,不能抛出,因为run()没有抛出异常 try { //休眠一秒 sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); }//public ThreadMethodTest(String string) { //super(string); //} }public class ThreadMethod { public static void main(String[] args) { //创建主线程名字 Thread.currentThread().setName("创建主线程名字:"); System.out.println(Thread.currentThread().getName()); //创建主线程名字://启动线程,并将线程命名 ThreadMethodTest threadMethodTest1 = new ThreadMethodTest(); threadMethodTest1.start(); //Thread-0ThreadMethodTest threadMethodTest2=new ThreadMethodTest(); threadMethodTest2.setName("这是一个线程!"); threadMethodTest2.start(); //这是一个线程! // //通过构造器的方法将线程命名 //ThreadMethodTest threadMethodTest3 = new ThreadMethodTest("通过构造器给线程命名!"); //我们在main中加入ThreadMethodTest线程 for (int i=0; i<2; i++){ System.out.println(Thread.currentThread().getName()); if (i==0){ try { threadMethodTest1.join(); } catch (InterruptedException e) { e.printStackTrace(); } } } System.out.println(threadMethodTest1.isAlive()); //这是一个线程! //创建主线程名字: //false} }

线程的优先级
  • 线程的优先级等级
    1. MAX_PRIORITY**:**10
    2. MIN _PRIORITY**:**1
    3. NORM_PRIORITY**:**5
  • 涉及的方法
    1. getPriority() **:**返回线程优先值
    2. setPriority(int newPriority) **:**改变线程的优先级
  • 说明
    1. 线程创建时继承父线程的优先级
    2. 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
package com.hyb.thread; /** * @program: ThreadPriority * * @description:线程优先级 * @author: Huang Yubin * @create: 2021-06-05 18:24 **/class ThreadPriority1 extends Thread{ @Override public void run() { System.out.println("这是子线程的优先级!"); } }public class ThreadPriority { public static void main(String[] args) { ThreadPriority1 threadPriority1 = new ThreadPriority1(); threadPriority1.setPriority(Thread.MAX_PRIORITY); threadPriority1.start(); System.out.println(Thread.currentThread().getName()); //因为概率问题 //main //这是子线程的优先级! } }

线程练习
package com.hyb.thread; /** * @program: CreateThread2 * @description:买票*忽略该线程的安全问题 * @author: Huang Yubin * @create: 2021-06-05 18:41 **/class ThisThread2 extends Thread{ private static int ticket=5; @Override public void run() { while (true){ //下面如何调换两个条件的位置,会出现错误,至于为什么,我也不知道 //好像是static的影响 if (ticket>0){ System.out.println(ticket); ticket--; }else { break; } } } }public class CreateThread2 { public static void main(String[] args) { ThisThread2 thisThread2_1 = new ThisThread2(); ThisThread2 thisThread2_2 = new ThisThread2(); ThisThread2 thisThread2_3 = new ThisThread2(); thisThread2_1.start(); thisThread2_2.start(); thisThread2_3.start(); //5 //4 //3 //2 //1 //5 //5 } }

从上面我们可以看到,要是用一个线程执行多种功能,必须声明static,那有什么方法定义一种线程让其就拥有本例题的功能呢?
下面就是创建线程的另一种方法了
创建线程的另一种方式
package com.hyb.thread; import java.util.Currency; /** * @program: OtherThread * @description:其他方式创建线程 * @author: Huang Yubin * @create: 2021-06-06 08:54 **/ //实现接口Runnable class OtherThread implements Runnable{ //重写run()方法 @Override public void run() { for (int i = 0; i < 10; i++) { if (i%2==0){ //下面这条语句如果要获得线程名,就不能直接getName获取了,因为OtherThread是继承Object类的,不是继承Runnable类的。 //要获取,得通过Thread.currentThread().getName() System.out.println(i); } } } }public class OtherCreateThread { //new一个OtherThread对象 public static void main(String[] args) {OtherThread otherThread = new OtherThread(); /*----------------------------------*/ Thread thread1 = new Thread(otherThread); /*鼠标滑轮点击进去API后,Ctrl+inset可以查看方法目录,直接搜索即可索引到哪个方法里*/ //将对象otherThread放在Thread的构造器中 //public Thread(Runnable target) { //init(null, target, "Thread-" + nextThreadNum(), 0); //}//public void run() { //if (target != null) { //target.run(); //} //} //上面是Thread的run方法,我们可以看到,如果target不等于null,就会调用target的run, //然后现在这个target是一个Runnable下的对象,自然也就是调用该对象里我们重写的run了/*-----------------------------*/ //为线程命名 thread1.setName("这是Runnable创建线程的方式!"); //启动线程 thread1.start(); //在创建一个线程,就不用再new一个OtherThread对象了 //我们直接再放进去一次构造器中就行了 Thread thread2=new Thread(otherThread); thread2.setName("这是第二个线程!"); thread2.start(); //0 //2 //4 //6 //8 //0 //2 //4 //6 //8 } //通过上面的方法,我们在用第一种方式去买票,就不用生命static了,而且可以直接new一个实现类的对象就行了。

两种创建线程方式的联系和开发优先 开发优先
  • 优先选用实现Runnable接口的方式
  • 避免了单继承的限制
    1. 因为我们都知道通过继承Thread类的方式实现,有一种弊病
      Java是单继承的,万一它本身就有直接父类,这不就尴尬了吗?
      所以我们优先选用接口实现方式。
两者联系
  • 我们可以利用鼠标滑轮点击Thread进入查看源代码,是这样的:
    class Thread implements Runnable
    这说明,Thread类也是实现Runnable接口的一个类。
    无论我们用哪种方式,本质上都是重写Runnable接口的run方法。
线程的生命周期
  • JDK中用Thread.State****类定义了线程的几种状态
    要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类
    及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五
    种状态:
  • 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建
    状态
  • **就绪:**处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已
    具备了运行的条件,只是没分配到CPU资源
  • **运行:**当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线
    程的操作和功能
  • **阻塞:**在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中
    止自己的执行,进入阻塞状态
  • **死亡:**线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
java学习|Java高级学习笔记-1(多线程)
文章图片

线程安全问题:线程的同步
  • 前面我们用到的例子买票,会出现同票的情况,这就是线程安全问题的一个例子。
  • 线程安全为什么会出现?
    1. 在大数据时代,我们一直听到一个词,叫共享。
    2. 那么在共享的时刻,一个线程是阻塞状态,另一个线程启动了,这就会造成共用一个数据
    3. 举个简单的例子,夫妻二人分别取同一账户的钱总共两千,当丈夫刚好取的时候,机器卡了,妻子刚好此刻取钱,这样可能会取出同样的两千块。
解决线程安全问题 引入例子
  • 前面我们做的买票例子,会出现线程安全问题,也就是同票,那这种线程是怎样产生的呢?
  • 首先我们买票会出现一个极端例子,图例:
java学习|Java高级学习笔记-1(多线程)
文章图片

可以看到,当我们就剩下最后一张票的时候,线程t1,t2,t3都被阻塞了,这个时候,t1万一先打印车票,轮到t2,那不就是变成0了,这样,t3也就变成-1了。虽然我们都知道有if语句判断tick是否大于0,但这是线程同步参与进来了,也就是if语句都判断了之后才执行的。
解决安全问题
  • 那么我们该如何解决以上的线程安全问题?
    1. 首先得明确它的问题,也就是当某个线程正在工作的时候,其他线程也参与进来了,就会造成抢同一数据状态
    2. 要解决这种问题,就要当一个线程工作时,其他线程不能参与进来。
    3. 在Java中,我们提供一种同步机制来解决线程安全问题
方式一:同步代码块
  • synchronized(同步监听器){
    //需要被同步的代码
    }
  • 说明:
    1. 操作共享的数据的,即为被同步的代码
    2. 共享数据:多个线程共同操作的数据。比如,上面卖票程序中ticket即为共享的数据
    3. 同步监听器,俗称锁,任何一个对象都可以作为一个锁。但是多个线程必须共用一把锁。
    4. 不同的多线程实现方法,解决安全问题是不一样的
实现Runnable接口
package com.hyb.thread; /** * @program: ThreadSecurity * @description:线程安全例子,卖票 * @author: Huang Yubin * @create: 2021-06-06 10:21 **///选择Runnable接口实现的方法 class Windows implements Runnable{ //用了这个方法我们就不用声明变量为static了 private int ticket=10; //我们在这里声明一个锁,这个锁是大伙共用,所以我们必须声明在这里,如果在run方法里,那就不是同一个了,因为每一次调用run就会声明一次,自然不是同一个了 Object object=new Object(); @Override public void run() { while (true){ //这里我们将锁放进去,记住是任何一个对象都可以,但是这些线程必须得共用一个对象 synchronized(object){ if (ticket>0){ System.out.println(ticket); ticket--; }else break; } } } } public class ThreadSecurity { public static void main(String[] args) { Windows windows = new Windows(); Thread thread1= new Thread(windows); Thread thread2= new Thread(windows); Thread thread3= new Thread(windows); thread1.start(); thread2.start(); thread3.start(); //10 //9 //8 //7 //6 //5 //4 //3 //2 //1 }}

继承Thread类
package com.hyb.thread; /** * @program: ThreadSecurity线程安全例子,卖票 * @description: * @author: Huang Yubin * @create: 2021-06-06 10:21 **///这里我们选择实现Thread类的方式class Windows extends Thread{ private static int ticket=10; /*在这里创建一个对象,就不能用锁,因为不是唯一,main方法里每new一个对象,这里也都会产生一个对象*/ /*如果非得使用,那么和ticket变量声明一样,变成static就行了,因为static是随着类的加载而加载的,而且类只能加载一次*/ /*当然的,如何非得不加static这样声名,只能用在实现接口Runnable实现多线程方法中使用了*/ //Object object=new Object(); @Override public void run() { while (true){/*在这里用this也不行,因为this代表本类下的对象,而本类下我们在main放里new了几个对象*/ /*但是我们在Runnable接口实现多线程这种方法中可以用this,因为在这种方法里,我们在main里只new一个本类下的对象*/ //synchronized (this){}/*所以我们只能用本类来充当这个锁 本类.class */ synchronized (Windows.class){ if (ticket>0){ System.out.println(ticket); ticket--; }else break; } } } } public class ThreadSecurity { public static void main(String[] args) { Windows windows1 = new Windows(); Windows windows2 = new Windows(); Windows windows3 = new Windows(); windows1.start(); windows2.start(); windows3.start(); //10 //9 //8 //7 //6 //5 //4 //3 //2 //1 }}

  • 我们可以看到,上面例子的结果不可能出现重复的了,他们的原理是:
    当多个线程进去的时候,锁开始执行,进去了的线程就进去了,不会被其他线程抢占。
    等到已进去的线程再出来,三个线程再开始抢占执行权,记住这个时候也可能是第一个线程执行,
    因为他只是将执行权再释放了而已,并不代表下一次没有执行权了。
  • 但这种方式也有一种缺点,就是每次只能执行一次线程,其他线程在等待,就造成了效率降低。
    但是效率降低了,并不代表我们在实际应用不能使用。
  • 我们可以看到,用实现多线程的不同方式去处理线程安全问题时,写代码的方式是不一样,但逻辑都是一样
    我们记住一点,每次构造的锁都必须是唯一的就行了。
方式二:同步方法
  • 如果需要同步的代码恰好能变成一个方法,那我们不妨将他定义为同步方法
Runnable接口实现多线程
package com.hyb.thread; /** * @program: ThreadSecurity线程安全例子,卖票 * @description: * @author: Huang Yubin * @create: 2021-06-06 10:21 **///这里我们通过Runnable实现多线程 class Windows implements Runnable{ private int ticket=10; @Override public void run() { while (true){ show(); } }/*下面的方法不用的锁代表this,this又代表本类下的对象,而我们在main里只new了一个对象,所以是唯一的*/ private synchronized void show() { if (ticket>0){ System.out.println(ticket); ticket--; } } }public class ThreadSecurity { public static void main(String[] args) { Windows windows = new Windows(); Thread thread1=new Thread(windows); Thread thread2=new Thread(windows); Thread thread3=new Thread(windows); thread1.start(); thread2.start(); thread3.start(); //10 //9 //8 //7 //6 //5 //4 //3 //2 //1 }}

继承Thread类实现多线程
package com.hyb.thread; /** * @program: ThreadSecurity线程安全例子,卖票 * @description: * @author: Huang Yubin * @create: 2021-06-06 10:21 **///这里我们选择实现Thread类的方式class Windows extends Thread{ private static int ticket=10; @Override/*理论上run这个方法自己也可以声明为同步的 * 但是在这个问题当中,我们要同步的代码量只能是while循环里的语句 * 如果包含了while循环,就变成了其中一个线程直接全部将票卖完了*/ public void run() { while (true){ //我们直接将要同步的代码块,变成一个方法 show(); } }/*这个同步方法为什么要声明为静态的说明: * 1,如果声明为static,这里的锁就代表是this了,this又代表本类下对象 * 2,而这是继承Thread类来实现多线程,我们在main里要new多个对象,这就造成this这个锁代表,t1,t2,t3了,不是唯一的 * 3,声明为static,这个锁就代表本类Windows了,这是唯一的,因为类只能加载一次,而且static是随着类的加载而加载的 * */ private static synchronized void show() { if (ticket>0){ System.out.println(ticket); ticket--; } } } public class ThreadSecurity { public static void main(String[] args) { Windows windows1 = new Windows(); Windows windows2 = new Windows(); Windows windows3 = new Windows(); windows1.start(); windows2.start(); windows3.start(); //10 //9 //8 //7 //6 //5 //4 //3 //2 //1 }}

  • 在同步方法里解决线程安全问题,还是存在同步监视器,也就是锁,不过这是隐式声明。
  • 在非静态的同步方法里,锁是this,表示本类下的对象。
  • 在静态的同步方法里,锁就是本类,可用本类.class表示,一定是唯一的。
方式三: Lock
  • 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同
    步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的
    工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象
    加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和
    内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以
    显式加锁、释放锁。
package com.hyb.thread; import java.util.concurrent.locks.ReentrantLock; /** * @program: JDkLock * @description:jdk5.0新特性,提供lock来解决线程安全问题 * @author: Huang Yubin * @create: 2021-06-06 15:06 **/class LockThread implements Runnable{ private int ticket=10; //new一个对象 ReentrantLock lock=new ReentrantLock(); //里面的构造器初始值是false @Override public void run() { while (true){ //调用ReentrantLock的lock方法 lock.lock(); try { if (ticket>0){ System.out.println(ticket); ticket--; }else break; } finally { //关闭这个锁 lock.unlock(); } } } }public class JDkLock { public static void main(String[] args) { LockThread lockThread = new LockThread(); Thread thread = new Thread(lockThread); thread.start(); //10 //9 //8 //7 //6 //5 //4 //3 //2 //1 } }

  • synchronized 与 Lock 的对比:
    1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是
      隐式锁,出了作用域自动释放
    2. Lock只有代码块锁,synchronized有代码块锁和方法锁
    3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有
      更好的扩展性(提供更多的子类)
  • 优先使用顺序:
    Lock -> 同步代码块(已经进入了方法体,分配了相应资源) ->同步方法
    (在方法体之外)
单例模式的线程安全问题
  • 我们都知道单例模式有两种方式创建,分别是饿汉式和懒汉式,但是
package com.hyb.thread; /** * @program: SingleThread * @description:单例模式下的线程安全问题 * @author: Huang Yubin * @create: 2021-06-06 12:53 **/class Single{ private Single(){} private static Single single=null; public static Single getSingle(){ //下面的写法效率高 if (single==null){ synchronized (Single.class){ if (single==null){ single=new Single(); } } } return single; //return show(); }/*首先如果我们不加一个锁,加入三个线程一同进来,就会new多个不同的对象*/ /*所以我们要加上一个锁,只要第一个进去的线程new了一个对象,new完了之后,后来的线程就会发现single不是null,就一直return了*/ /*但下面的就效率会变低,因为我们第一个已经new了一个对象,后面我们始终都要判断他是否为空,我们可以规定,当第一个new完一个对象后, * 后面的线程就直接return就可以了,就无需判断了。*/ //private synchronized static Single show() { //下面的效率低一点 //if (single==null){ //single= new Single(); //} //return single; //}}public class SingleThread {}

薛定谔监听器
  • 薛定谔监听器俗称死锁,就如同死循环一般,程序不报错,但是我们不能允许他存在。
package com.hyb.thread; import java.util.Queue; /** * @program: DeadLock * @description: * @author: Huang Yubin * @create: 2021-06-06 14:35 **/ public class DeadLock { 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("b"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2){ s1.append("c"); s2.append("d"); } } } }.start(); new Thread( new Runnable(){ @Override public void run() { synchronized (s2){ s1.append("e"); s2.append("d"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s1){ s1.append("f"); s2.append("g"); } } } }).start(); } }

  • 上面的例子可能是正确的,但也可能出现薛定谔监视器,我们Sleep就可以加大它出现薛定谔监视器的概率。
  • 它原理便是,当一个线程握住一个锁之后,被休眠了,那么另一个线程就会有可能开始,但是这个线程又要握住被休眠的线程里的东西,就造成了僵持情况。
    1. 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃
      自己需要的同步资源,就形成了线程的死锁
    2. 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于
      阻塞状态,无法继续
  • 解决方法:
    1. 专门的算法、原则
    2. 尽量减少同步资源的定义
    3. 尽量避免嵌套同步
线程练习1
  • 银行有一个账户,有两个储户分别向银行里存3000块钱,每次存一千,存三次,每次存完打印余额
    1. 是多线程问题吗?是,有两个储户
    2. 是否有共享数据?是,账户
    3. 是否有线程安全问题?有
    4. 如何解决线程安全问题?
      1. 使用lock
      2. 同步代码
      3. 同步方法
  • 首先我们要解决这个问题,必须要知道如何解决共同账户的问题。
    我们可以造一个类Account,叫账户,然后让造一个类Customer实现
package com.hyb.thread; /** * @program: ThreadPractice * @description:线程练习 * @author: Huang Yubin * @create: 2021-06-06 15:28 **/class Account implements Runnable{ private int Money; Account(int money) { Money = money; }@Override publicsynchronized void run() { for (int i=0; i<3; i++){ Money+=1000; System.out.println("存钱成功!"+Thread.currentThread().getName()+"的余额为:"+Money); } } }public class ThreadPractice1 { public static void main(String[] args) { Account account = new Account(0); Thread thread1 = new Thread(account); Thread thread2=new Thread(account); thread1.setName("甲:"); thread2.setName("乙:"); thread1.start(); thread2.start(); //存钱成功!甲:的余额为:2000 //存钱成功!乙:的余额为:2000 //存钱成功!甲:的余额为:3000 //存钱成功!乙:的余额为:4000 //存钱成功!乙:的余额为:5000 //存钱成功!甲:的余额为:6000 //不处理,存在线程安全问题 /*以下是处理之后的结果*/ //存钱成功!甲:的余额为:1000 //存钱成功!甲:的余额为:2000 //存钱成功!甲:的余额为:3000 //存钱成功!乙:的余额为:4000 //存钱成功!乙:的余额为:5000 //存钱成功!乙:的余额为:6000} }

线程通信
  • 线程通信就是两个线程的交互输出
  • 主要涉及到wait() 与 notify() 和 **notifyAll()**的调用
    1. wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当
      前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有
      权后才能继续执行。
    2. notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
    3. notifyAll ():唤醒正在排队等待资源的所有线程结束等待
  • 这三个方法只有在synchronized方法或synchronized代码块中才能使用,必须是同一监视器,否则会报
    java.lang.IllegalMonitorStateException异常
  • 因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,
    因此这三个方法只能在Object类中声明。
引入例子
package com.hyb.thread; /** * @program: ThreadCommunication * @description:线程的通信 * @author: Huang Yubin * @create: 2021-06-06 16:09 **/class Communication implements Runnable{ private int ticket=10; Object object=new Object(); @Override public void run() { while (true){/*第一次线程进来的时候,没有唤醒,因为首先是没有线程睡眠的。 * 第一次线程拿到锁后,执行操作,然后遇到wait,阻塞,释放这把锁。 * 下一个线程进来,唤醒上一个线程,但是自己已经拿到锁了,继续进去,执行操作 * 这就造成了交替打印的结果*/ synchronized (object) {//这里是this调的 //唤醒上一个线程 object.notify(); if (ticket>0){ System.out.println(Thread.currentThread().getName()+":"+ticket); ticket--; try { //等待,释放锁 object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else break; } } } }public class ThreadCommunication { public static void main(String[] args) { Communication communication = new Communication(); Thread t1=new Thread(communication); Thread t2=new Thread(communication); t1.setName("t1:"); t2.setName("t2: "); t1.start(); t2.start(); //t1::10 //t2: :9 //t1::8 //t2: :7 //t1::6 //t2: :5 //t1::4 //t2: :3 //t1::2 //t2: :1} }

Sleep与wait的异同
  • 两个方法声名的位置不同,Sleep在Thread类,而wait是声明在Object类下
  • 调用的要求不同,Sleep在任何需要的场景下都可以使用,但wait只能在同步代码块中使用
  • 关于释放监视器:Sleep不可以,后者可以
多线程经典问题
  • 生产者/消费者问题
    • 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处
      取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图
      生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通
      知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如
      果店中有产品了再通知消费者来取走产品。
    • 这里可能出现两个问题:
      1. 生产者比消费者快时,消费者会漏掉一些数据没有取到。
      2. 消费者比生产者快时,消费者会取相同的数据。
  • 分析题目
    1. 该题目是不是多线程的?是,消费者和生产者
    2. 是否存在共同数据?有,店员或者是产品
    3. 是否存在线程安全问题?有,当生产者阻塞的时候,消费者立马消费了,就不能显示出生产的产品
    4. 是否存在线程通信?存在,我们要通过生产者和消费互相告知,是否要生产
package com.hyb.thread; /** * @program: ThreadProduct * @description:生产和消费者问题 *经典例题:生产者/消费者问题 * ?生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处 * 取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图 * 生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通 * 知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如 * 果店中有产品了再通知消费者来取走产品。 * ?这里可能出现两个问题: * ?生产者比消费者快时,消费者会漏掉一些数据没有取到。 * ?消费者比生产者快时,消费者会取相同的数据 * @author: Huang Yubin * @create: 2021-06-06 16:56 **/class Clerk{//店员这里只有产品 private int product; //提供构造器 public Clerk(int product) { this.product = product; }//叫生产者生产 public synchronized void ProducerProduct(){ if (product<20){ product++; System.out.println(Thread.currentThread().getName()+"开始生产"+product+"号产品"); //如果生产了产品叫消费者去消费 notify(); }else{ try { //如果产品太多了,被叫停 wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }//叫消费者消费 public synchronized void ConsumerProduct(){ if (product>0){ System.out.println(Thread.currentThread().getName()+"开始消费"+product+"号产品"); product--; //如果消费完成,叫生产者生产 notify(); }else { try { //如果没有产品,消费者等待一下 wait(); } catch (InterruptedException e) { e.printStackTrace(); } }} }class Producer implements Runnable{private Clerk clerk; public Producer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } clerk.ProducerProduct(); } } }class Consumer implements Runnable{private Clerk clerk; public Consumer(Clerk clerk) { this.clerk = clerk; }@Override public void run() { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } clerk.ConsumerProduct(); } } }public class ThreadProduct { public static void main(String[] args) { Clerk clerk = new Clerk(0); Producer producer = new Producer(clerk); Consumer consumer = new Consumer(clerk); Thread threadProducer=new Thread(producer); Thread threadConsumer=new Thread(consumer); threadConsumer.setName("消费者一号:"); threadProducer.setName("生产者一号:"); threadProducer.start(); threadConsumer.start(); //生产者一号:开始生产1号产品 //生产者一号:开始生产2号产品 //消费者一号:开始消费2号产品 //生产者一号:开始生产2号产品 //消费者一号:开始消费2号产品 //生产者一号:开始生产2号产品 //消费者一号:开始消费2号产品 //生产者一号:开始生产2号产品 //消费者一号:开始消费2号产品} }

JDk5.0新特性 实现Callable接口
  • 与使用Runnable相比, Callable功能更强大些
    1. Callable支持泛型
    2. 相比run()方法,可以有返回值
    3. 方法可以抛出异常
    4. 支持泛型的返回值
    5. 需要借助FutureTask类,比如获取返回结果
  • Futrue接口:
    1. 可以对具体Runnable、Callable任务的执行结果进行取消、查询是
      否完成、获取结果等。
    2. FutrueTask是Futrue接口的唯一的实现类
    3. FutureTask 同时实现了Runnable, Future接口。它既可以作为
      Runnable被线程执行,又可以作为Future得到Callable的返回值
package com.hyb.thread; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; /** * @program: NewJDK_5 * @description:jdk5.0新增方式实现多线程callnable * @author: Huang Yubin * @create: 2021-06-07 21:14 **/class New implements Callable{ @Override public Object call() throws Exception { int sum=0; for (int i = 0; i < 100; i++) { if (i%2==0){ sum+=i; } } return sum; } }public class NewJDK_5 { public static void main(String[] args) { New aNew = new New(); FutureTask futureTask = new FutureTask(aNew); new Thread(futureTask).start(); try { System.out.println(futureTask.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }

线程池
  • **背景:**经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,
    对性能影响很大。
  • **思路:**提前创建好多个线程,放入线程池中,使用时直接获取,使用完
    放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交
    通工具。
  • 好处:
    1. 提高响应速度(减少了创建新线程的时间)
    2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    3. 便于线程管理
      1. corePoolSize:核心池的大小
      2. maximumPoolSize:最大线程数
      3. keepAliveTime:线程没有任务时最多保持多长时间后会终止
  • 例子:手机页面的浏览,每一条信息都会加载一个图片,为了防止出现卡顿,我们需要用线程池,将每一条信息都装进一个线程里。
  • JDK 5.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.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运
      【java学习|Java高级学习笔记-1(多线程)】行命令或者定期地执行。
package com.hyb.thread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; /** * @program: ThreadPool * @description:线程池 * @author: Huang Yubin * @create: 2021-06-08 11:55 **/class NumberThrea implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { if (i%2==0){ System.out.println(i); } } } }public class ThreadPool { public static void main(String[] args) { //提供指定线程池数显 ExecutorService number= Executors.newFixedThreadPool(10); //因为ExecutorService是一个接口,所以当我们要设置线程的属性,要通过其实现类来实现,只能强制转换 //我们可以查看一下他的实现类是什么 /*System.out.println(number.getClass()); */ //ThreadPoolExecutor //然后强制转换 /*ThreadPoolExecutor number1=(ThreadPoolExecutor)number; *//*启动线程*/ number.execute(new NumberThrea()); //适合与Runnable接口 //number.submit()适用于Callable接口/*关闭线程池*/ number.shutdown(); }}

    推荐阅读