Java多线程代码编写

Java多线程代码编写

什么是多线程

并发和并行

 并行:指两个或多个时间在同一时刻发生(同时发生);

 并发:指两个或多个事件在一个时间段内发生。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

  而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。

  目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定不能并行的处理多个任务,只能是多个任务交替的在单个 CPU 上运行。

进程和线程

  进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  线程:进程内部的一个独立执行单元;一个进程可以同时并发的运行多个线程,可以理解为一个进程便相当于一个单 CPU 操作系统,而线程便是这个系统中运行的多个任务。

我们可以再电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程和线程

线程选项:

进程和线程的区别

  进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。

  线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

注意:1、因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。

   2、Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。

   3、由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。

线程理解:线程是一个程序里面不同的执行路径

 每一个分支都叫做一个线程,main()叫做主分支,也叫主线程。

  程只是一个静态的概念,机器上的一个.class文件,机器上的一个.exe文件,这个叫做一个进程。程序的执行过程都是这样的:首先把程序的代码放到内存的代码区里面,代码放到代码区后并没有马上开始执行,但这时候说明了一个进程准备开始,进程已经产生了,但还没有开始执行,这就是进程,所以进程其实是一个静态的概念,它本身就不能动。平常所说的进程的执行指的是进程里面主线程开始执行了,也就是main()方法开始执行了。进程是一个静态的概念,在我们机器里面实际上运行的都是线程。

  Windows操作系统是支持多线程的,它可以同时执行很多个线程,也支持多进程,因此Windows操作系统是支持多线程多进程的操作系统。Linux和Uinux也是支持多线程和多进程的操作系统。DOS就不是支持多线程和多进程了,它只支持单进程,在同一个时间点只能有一个进程在执行,这就叫单线程。

  CPU难道真的很神通广大,能够同时执行那么多程序吗?不是的,CPU的执行是这样的:CPU的速度很快,一秒钟可以算好几亿次,因此CPU把自己的时间分成一个个小时间片,我这个时间片执行你一会,下一个时间片执行他一会,再下一个时间片又执行其他人一会,虽然有几十个线程,但一样可以在很短的时间内把他们通通都执行一遍,但对我们人来说,CPU的执行速度太快了,因此看起来就像是在同时执行一样,但实际上在一个时间点上,CPU只有一个线程在运行。

 

多线程的优势

  1、进程之间不能共享内存,而线程之间可以共享内存。

  2、系统创建进程需要为该进程重新分配系统资源,创建线程的代价则小的多,因此多任务并发时,多线程效率高。

  3、Java 语言本身内置多线程功能的支持,而不是单纯作为底层系统的调度方式,从而简化了多线程编程。

  注意:多线程是为了同步完成多个任务,不是为了提高程序运行效率,而是通过提高资源使用效率来提高系统的效率。

什么才是真正的多线程?如果你的机器是双CPU,或者是双核,这确确实实是多线程。

 

创建进程和线程

  在 windows 操作系统中,我们创建一个进程通常就是打开某个应用软件,这便在电脑中创建了一个进程。更原始一点的,我们在命令提示符中来做(我们以打开记事本这个进程为例):

  第一步:windows+R,输入cmd,打开 cmd 命令提示符

   

  第二步:在命令提示符中输入 notepad,按 Enter 键便会弹出记事本应用软件

   

 PS:常用的windows 应用软件命令

    1、regedit:打开注册表编辑器

    2、control:打开控制面板

    3、msconfig:打开系统配置

    4、gpedit.msc:打开本地组策略

    5、explorer:打开资源管理器

    6、taskmgr:任务管理器

    7、logoff:直接注销计算机

    8、osk:打开屏幕键盘

    9、calc:打开计算器

    10、mspaint:调出画图软件

    11、dxdiag:查看电脑详细配置信息

    12、mstsc:打开远程桌面连接

    13、systeminfo:查看计算机基本信息

    14、notepad:打开记事本

在java中创建进程

第一种方法:通过 Runtime 类的 exec() 方法来创建进程

public class Runtime
extends Object
①、表示当前进程所在的虚拟机实例,每个Java应用程序都有一个Runtime类的Runtime ,允许应用程序与运行应用程序的环境进行接口。
②、由于任何进程只会运行与一个虚拟机实例当中,即只会产生一个虚拟机实例(底层源码采用 单例模式)
③、当前运行时可以从getRuntime方法获得。

  由上面源码可以看到,构造器私有化了,即外部我们不能 new 一个新的 Runtime 实例,而内部给了我们一个获取 Runtime 实例的方法 getRuntime() 。

通过 Runtime 类创建一个 记事本的 进程

public class ProcessTest {
     
    public static void main(String[] args) throws Exception {
        Runtime run = Runtime.getRuntime();
        //打开记事本
        run.exec("notepad");
    }
 
}

第二种方法:通过 ProcessBuilder 创建进程

public final class ProcessBuilder
extends Object<br>①、此类用于创建操作系统进程。
②、每个ProcessBuilder实例管理进程属性的集合。 start()方法使用这些属性创建一个新的Process实例。 start()方法可以从同一实例重复调用,以创建具有相同或相关属性的新子进程。

public class ProcessTest {
     
    public static void main(String[] args) throws Exception {
        //打开记事本
        ProcessBuilder pBuilder = new ProcessBuilder("notepad");
        pBuilder.start();
    }
 
} 

在java中创建进程

第一种方法:继承 Thread 类

public class Thread
extends Object
implements Runnable  

步骤:1、定义一个线程类 A 继承于 java.lang.Thread 类

   2、在 A 类中覆盖 Thread 类的 run() 方法

   3、在 run() 方法中编写需要执行的操作

   4、在 main 方法(线程)中,创建线程对象,并启动线程

      创建线程类:A类 a = new A()类;

      调用 start() 方法启动线程:a.start();

package com.ys.thread;
 
class Thread1 extends Thread{
    @Override
    public void run() {
        for(int i = 0 ; i < 10 ;i++){
            System.out.println("播放音乐"+i);
        }
    }
}
 
public class ThreadTest {
    public static void main(String[] args) {
        for(int i = 0 ; i < 10 ; i++){
            System.out.println("玩游戏"+i);
            if(i==5){
                Thread1 th1 = new Thread1();
                th1.start();
            }
        }
    }
 
}

 结果:

玩游戏0
玩游戏1
玩游戏2
玩游戏3
玩游戏4
玩游戏5
玩游戏6
玩游戏7
玩游戏8
玩游戏9
播放音乐0
播放音乐1
播放音乐2
播放音乐3
播放音乐4
播放音乐5
播放音乐6
播放音乐7
播放音乐8
播放音乐9

  注意:我们看结果,并不是出现 5 个先打游戏,然后在播放音乐,这是线程调度的结果,两个线程同时在争抢 CPU 的资源,即最后的结果,前面5个打游戏的必然先出现的,后面的啥时候出现播放音乐就看 CPU 怎么调度了,这是随机的。我们不能干涉。

第二种方法:实现 Runnable 接口

@FunctionalInterface
public interface Runnable

1、Runnable接口应由任何类实现,其实例将由线程执行。 该类必须定义一个无参数的方法,称为run 。 
2、该接口旨在为希望在活动时执行代码的对象提供一个通用协议。此类整个只有一个 run() 抽象方法

 

步骤:1、定义一个线程类 A 实现于 java.lang.Runnable 接口(注意:A类不是线程类,没有 start()方法,不能直接 new A 的实例启动线程)

   2、在 A 类中覆盖 Runnable 接口的 run() 方法

   3、在 run() 方法中编写需要执行的操作

   4、在 main 方法(线程)中,创建线程对象,并启动线程

      创建线程类:Thread t = new Thread( new A类() ) ;

      调用 start() 方法启动线程:t.start();

class Runnable1 implements Runnable{
    @Override
    public void run() {
        for(int i = 0 ; i < 10 ;i++){
            System.out.println("播放音乐"+i);
        }
    }
}
 
public class RunnableTest {
    public static void main(String[] args) {
        for(int i = 0 ; i < 10 ; i++){
            System.out.println("玩游戏"+i);
            if(i==5){
                Thread th1 = new Thread(new Runnable1());
                th1.start();
            }
        }
    }
 
}

第三种方法:使用匿名内部类创建线程

public static void main(String[] args) {
        for(int i = 0 ; i < 10 ; i++){
            System.out.println("玩游戏"+i);
            if(i==5){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for(int i = 0 ; i < 10 ;i++){
                            System.out.println("播放音乐"+i);
                        }
                    }
                }).start();
            }
        }
    }

注意:

1、启动线程是调用 start() 方法,而不是 调用 run() 方法。

  解析:run()方法:在本线程内调用run()方法,和其他方法没有什么区别,可以重复多次调用;

     start()方法:启动一个线程,实际上还是调用该Runnable对象的run()方法。

     打开 Thread 类的源码,start()方法里面有一句:

        private native void start0();  //native 关键字:本地程序调用    

    native关键字指的是Java本地接口调用,即是使用Java调用本地操作系统的函数功能完成一些特殊的操作,而这样的代码开发在Java中几乎很少出现,因为Java的最大特点是可移植性,如果一个程序 只能在固定的操作系统上使用,那么可移植性就将彻底丧失,多线程的实现一定需要操作系统的支持,那么start0()方法实际上就和抽象方法很类似,没有方法体,而是交给JVM 去实现,即在windows下的JVM可能使用A方法实现start0(),在linux下的JVM可能使用B方法实现start0(),在调用时并不会关心具体是何方式实现了start0()方法,只会关心最终的操作结果,交给 JVM去匹配了不同的操作系统。

2、不能多次启动同一个线程,即多次调用 start() 方法,只能调用一次,否则报错:

 

线程的同步

利用多线程模拟 3 个窗口卖票

第一种方法:继承 Thread 类

 创建窗口类 TicketSell 

public class TicketSell extends Thread{
    //定义一共有 50 张票,注意声明为 static,表示几个窗口共享
    private static int num = 50;
     
    //调用父类构造方法,给线程命名
    public TicketSell(String string) {
        super(string);
    }
    @Override
    public void run() {
        //票分 50 次卖完
        for(int i = 0 ; i < 50 ;i ++){
            if(num > 0){
                try {
                    sleep(10);//模拟卖票需要一定的时间
                } catch (InterruptedException e) {
                    // 由于父类的 run()方法没有抛出任何异常,根据继承的原则,子类抛出的异常不能大于父类, 故我们这里也不能抛出异常
                    e.printStackTrace();
                }
                System.out.println(this.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
            }
        }
    }
     
 
}

 创建主线程测试:

public class TestTicket {
 
    public static void main(String[] args) {
        //创建 3 个窗口
        TicketSell t1 = new TicketSell("A窗口");
        TicketSell t2 = new TicketSell("B窗口");
        TicketSell t3 = new TicketSell("C窗口");
         
        //启动 3 个窗口进行买票
        t1.start();
        t2.start();
        t3.start();
    }
}

  结果:这里我们省略了一些,根据电脑配置,结果会随机出现不同

B窗口卖出一张票,剩余48张
A窗口卖出一张票,剩余47张
C窗口卖出一张票,剩余49张
C窗口卖出一张票,剩余46张
B窗口卖出一张票,剩余44张
A窗口卖出一张票,剩余45张
A窗口卖出一张票,剩余43张
...
C窗口卖出一张票,剩余5张
A窗口卖出一张票,剩余4张
B窗口卖出一张票,剩余3张
A窗口卖出一张票,剩余2张
C窗口卖出一张票,剩余3张
B窗口卖出一张票,剩余1张
C窗口卖出一张票,剩余0张
A窗口卖出一张票,剩余-1张

第二种方法:实现 Runnable 接口

  创建窗口类 TicketSellRunnable

public class TicketSellRunnable implements Runnable{
 
    //定义一共有 50 张票,继承机制开启线程,资源是共享的,所以不用加 static
    private int num = 50;
     
    @Override
    public void run() {
        //票分 50 次卖完
        for(int i = 0 ; i < 50 ;i ++){
            if(num > 0){
                try {
                    //模拟卖一次票所需时间
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
            }
        }
    }
 
}

  创建主线程测试:

public class TicketSellRunnableTest {
    public static void main(String[] args) {
        TicketSellRunnable t = new TicketSellRunnable();
         
        Thread t1 = new Thread(t,"A窗口");
        Thread t2 = new Thread(t,"B窗口");
        Thread t3 = new Thread(t,"C窗口");
         
        t1.start();
        t2.start();
        t3.start();
    }
 
}

 结果:同理为了篇幅我们也省略了中间的一些结果

B窗口卖出一张票,剩余49张
C窗口卖出一张票,剩余48张
A窗口卖出一张票,剩余49张
B窗口卖出一张票,剩余47张
A窗口卖出一张票,剩余45张
......
A窗口卖出一张票,剩余4张
C窗口卖出一张票,剩余5张
A窗口卖出一张票,剩余3张
B窗口卖出一张票,剩余2张
C窗口卖出一张票,剩余1张
B窗口卖出一张票,剩余0张
A窗口卖出一张票,剩余-2张
C窗口卖出一张票,剩余-1张

结果分析:这里出现了票数为 负数的情况,这在现实生活中肯定是不存在的,那么为什么会出现这样的情况呢?

  

解决办法分析:即我们不能同时让超过两个以上的线程进入到 if(num>0)的代码块中,不然就会出现上述的错误。我们可以通过以下三个办法来解决:

1、使用 同步代码块

2、使用 同步方法

3、使用 锁机制

①、使用同步代码块

语法:
synchronized (同步锁) {
    //需要同步操作的代码         
}
 
同步锁:为了保证每个线程都能正常的执行原子操作,Java 线程引进了同步机制;同步锁也叫同步监听对象、同步监听器、互斥锁;
Java程序运行使用的任何对象都可以作为同步监听对象,但是一般我们把当前并发访问的共同资源作为同步监听对象
 
注意:同步锁一定要保证是确定的,不能相对于线程是变化的对象;任何时候,最多允许一个线程拿到同步锁,谁拿到锁谁进入代码块,而其他的线程只能在外面等着
public void run() {
        //票分 50 次卖完
        for(int i = 0 ; i < 50 ;i ++){
            //这里我们使用当前对象的字节码对象作为同步锁
            synchronized (this.getClass()) {
                if(num > 0){
                    try {
                        //模拟卖一次票所需时间
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
                }
            }
             
        }
    }

②、使用 同步方法

语法:即用  synchronized  关键字修饰方法

@Override
    public void run() {
        //票分 50 次卖完
        for(int i = 0 ; i < 50 ;i ++){
            sell();
             
        }
    }
    private synchronized void sell(){
        if(num > 0){
            try {
                //模拟卖一次票所需时间
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
        }
    }

  注意:不能直接用 synchronized 来修饰 run() 方法,因为如果这样做,那么就会总是第一个线程进入其中,而这个线程执行完所有操作,即卖完所有票了才会出来。

死锁问题

解决线程死锁的问题最好只锁定一个对象,不要同时锁定两个对象

 /*这个小程序模拟的是线程死锁的问题*/
 public class TestDeadLock implements Runnable {
     public int flag = 1;
     static Object o1 = new Object(), o2 = new Object();
 
     public void run() {
        System.out.println(Thread.currentThread().getName() + "的flag=" + flag);
         /*
          * 运行程序后发现程序执行到这里打印出flag以后就再也不往下执行后面的if语句了 
          * 程序也就死在了这里,既不往下执行也不退出
          */
 
         /* 这是flag=1这个线程 */
         if (flag == 1) {
             synchronized (o1) {
                /* 使用synchronized关键字把对象01锁定了 */                try {
                     Thread.sleep(500);
                 } catch (InterruptedException e) {
                     e.printStackTrace();                 }
                 synchronized (o2) {
                     /*
                      * 前面已经锁住了对象o1,只要再能锁住o2,那么就能执行打印出1的操作了 
                      * 可是这里无法锁定对象o2,因为在另外一个flag=0这个线程里面已经把对象o1给锁住了 
                      * 尽管锁住o2这个对象的线程会每隔500毫秒睡眠一次,可是在睡眠的时候仍然是锁住o2不放的
                      */
                     System.out.println("1");
                 }
             }
         }
        /*
          * 这里的两个if语句都将无法执行,因为已经造成了线程死锁的问题 
         * flag=1这个线程在等待flag=0这个线程把对象o2的锁解开, 
          * 而flag=0这个线程也在等待flag=1这个线程把对象o1的锁解开           * 然而这两个线程都不愿意解开锁住的对象,所以就造成了线程死锁的问题
          */
 
         /* 这是flag=0这个线程 */
         if (flag == 0) {
             synchronized (o2) {
                 /* 这里先使用synchronized锁住对象o2 */
                 try {
                     Thread.sleep(500);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 synchronized (o1) {
                     /*
                      * 前面已经锁住了对象o2,只要再能锁住o1,那么就能执行打印出0的操作了 可是这里无法锁定对象o1,因为在另外一个flag=1这个线程里面已经把对象o1给锁住了 尽管锁住o1这个对象的线程会每隔500毫秒睡眠一次,可是在睡眠的时候仍然是锁住o1不放的
                      */
                     System.out.println("0");
                 }
             }
         }
     }
 
     public static void main(String args[]) {
         TestDeadLock td1 = new TestDeadLock();
         TestDeadLock td2 = new TestDeadLock();
         td1.flag = 1;
         td2.flag = 0;
         Thread t1 = new Thread(td1);
         Thread t2 = new Thread(td2);
         t1.setName("线程td1");
         t2.setName("线程td2");
         t1.start();
         t2.start();
     }
 }

③、使用 锁机制

public interface Lock

 主要方法:

常用实现类:

public class ReentrantLock
extends Object
implements Lock, Serializable<br>//一个可重入互斥Lock具有与使用synchronized方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。

例子:

package com.ys.thread;
 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class TicketSellRunnable implements Runnable{
 
    //定义一共有 50 张票,继承机制开启线程,资源是共享的,所以不用加 static
    private int num = 50;
    //创建一个锁对象
    Lock l = new ReentrantLock();
     
    @Override
    public void run() {
        //票分 50 次卖完
        for(int i = 0 ; i < 50 ;i ++){
            //获取锁
            l.lock();
            try {
                if(num > 0){
                //模拟卖一次票所需时间
                Thread.sleep(10);
                System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally{
                //释放锁
                l.unlock();
            }
             
             
        }
    }
    private void sell(){
         
    }
 
}

 

生产者和消费者

生产者和消费者模型为:生产者Producer 生产某个对象(共享资源),放在缓冲池中,然后消费者从缓冲池中取出这个对象。也就是生产者生产一个,消费者取出一个。这样进行循环。

产者--消费者问题问题:共享数据的不一致性/临界资源的保护

 第一步:我们先创建共享资源的类 Person,它有两个方法,一个生产对象,一个消费对象

public class Person {
    private String name;
    private int age;
     
    /**
     * 生产数据
     * @param name
     * @param age
     */
    public void push(String name,int age){
        this.name = name;
        this.age = age;
    }
    /**
     * 取数据,消费数据
     * @return
     */
    public void pop(){
        System.out.println(this.name+"---"+this.age);
    }
}

第二步:创建生产者线程,并在 run() 方法中生产50个对象

ublic class Producer implements Runnable{
    //共享资源对象
    Person p = null;
    public Producer(Person p){
        this.p = p;
    }
    @Override
    public void run() {
        //生产对象
        for(int i = 0 ; i < 50 ; i++){
            //如果是偶数,那么生产对象 Tom--11;如果是奇数,则生产对象 Marry--21
            if(i%2==0){
                p.push("Tom", 11);
            }else{
                p.push("Marry", 21);
            }
        }
    }
}

第三步:创建消费者线程,并在 run() 方法中消费50个对象

public class Consumer implements Runnable{
    //共享资源对象
    Person p = null;
    public Consumer(Person p) {
        this.p = p;
    }
     
    @Override
    public void run() {
        for(int i = 0 ; i < 50 ; i++){
            //消费对象
            p.pop();
        }
    }
}

  由于我们的模型是生产一个,马上消费一个,那期望的结果便是 Tom---11,Marry--21,Tom---11,Mary---21......   连续这样交替出现50次

但是结果却是:

Marry---21
Marry---21
Marry---21
Marry---21
Marry---21
......
Marry---21
Marry---21
Marry---21
Marry---21
Marry---21

为了让结果产生的更加明显,我们在共享资源的 pop() 和 push() 方法中添加一段延时代码

/**
     * 生产数据
     * @param name
     * @param age
     */
    public void push(String name,int age){
        this.name = name;
        try {
            //这段延时代码的作用是可能只生产了 name,age为nul,消费者就拿去消费了
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.age = age;
    }
    /**
     * 取数据,消费数据
     * @return
     */
    public void pop(){
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.name+"---"+this.age);
    }  

这个时候,结果如下:

Marry---11
Tom---21
Marry---11
Tom---21
Marry---11
Tom---21
Marry---11
Tom---21
......
Tom---11
Tom---21
Marry---11
Tom---21
Marry---11
Marry---21

结果分析:这时候我们发现结果全乱套了,Marry--21是固定的,Tom--11是固定的,但是上面的结果全部乱了,那这又是为什么呢?而且有很多重复的数据连续出现,那这又是为什么呢?

原因1:出现错乱数据,是因为先生产出Tom--11,但是消费者没有消费,然后生产者继续生产出name为Marry,但是age还没有生产,而消费者这个时候拿去消费了,那么便出现 Marry--11。同理也会出现 Tom--21

原因2:出现重复数据,是因为生产者生产一份数据了,消费者拿去消费了,但是第二次生产者生产数据了,但是消费者没有去消费;而第三次生产者继续生产数据,消费者才开始消费,这便会产生重复

解决办法1:生产者生产name和age必须要是一个整体一起完成,即同步。生产的中间不能让消费者来消费即可。便不会产生错乱的数据。这里我们选择同步方法(在方法前面加上 synchronized)

public class Person {
    private String name;
    private int age;
     
    /**
     * 生产数据
     * @param name
     * @param age
     */
    public synchronized void push(String name,int age){
        this.name = name;
        try {
            //这段延时代码的作用是可能只生产了 name,age为nul,消费者就拿去消费了
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.age = age;
    }
    /**
     * 取数据,消费数据
     * @return
     */
    public synchronized void pop(){
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.name+"---"+this.age);
    }
}

 结果如下:

Marry---21
Marry---21
Marry---21
Marry---21
Marry---21
Tom---11
Tom---11
......
Tom---11
Tom---11
Tom---11
Tom---11
Tom---11

问题:还是没有解决上面的问题2,出现重复的问题。期望的结果是 Tom---11,Marry--21,Tom---11,Mary---21......   连续这样交替出现50次。那如何解决呢?

解决办法:生产者生产一次数据了,就暂停生产者线程,等待消费者消费;消费者消费完了,消费者线程暂停,等待生产者生产数据,这样来进行。

 

同步锁池的概念:

  同步锁池:同步锁必须选择多个线程共同的资源对象,而一个线程获得锁的时候,别的线程都在同步锁池等待获取锁;当那个线程释放同步锁了,其他线程便开始由CPU调度分配锁

关于让线程等待和唤醒线程的方法,如下:(这是 Object 类中的方法)

 

wait():执行该方法的线程对象,释放同步锁,JVM会把该线程放到等待池中,等待其他线程唤醒该线程

notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待(注意锁池和等待池的区别)

notifyAll():执行该方法的线程唤醒在等待池中等待的所有线程,把线程转到锁池中等待。

注意:上述方法只能被同步监听锁对象来调用,这也是为啥wait() 和 notify()方法都在 Object 对象中,因为同步监听锁可以是任意对象,只不过必须是需要同步线程的共同对象即可,否则别的对象调用会报错:        java.lang.IllegalMonitorStateException

假设 A 线程和 B 线程同时操作一个 X 对象,A,B 线程可以通过 X 对象的 wait() 和 notify() 方法来进行通信,流程如下:

①、当线程 A 执行 X 对象的同步方法时,A 线程持有 X 对象的 锁,B线程在 X 对象的锁池中等待

②、A线程在同步方法中执行 X.wait() 方法时,A线程释放 X 对象的锁,进入 X 对象的等待池中

③、在 X 对象的锁池中等待锁的 B 线程获得 X 对象的锁,执行 X 的另一个同步方法

④、B 线程在同步方法中执行 X.notify() 方法,JVM 把 A 线程从等待池中移动到 X 对象的锁池中,等待获取锁

⑤、B 线程执行完同步方法,释放锁,等待获取锁的 A 线程获得锁,继续执行同步方法

那么为了解决上面重复的问题,修改代码如下:

public class Person {
    private String name;
    private int age;
     
    //表示共享资源对象是否为空,如果为 true,表示需要生产,如果为 false,则有数据了,不要生产
    private boolean isEmpty = true;
    /**
     * 生产数据
     * @param name
     * @param age
     */
    public synchronized void push(String name,int age){
        try {
            //不能用 if,因为可能有多个线程
            while(!isEmpty){//进入到while语句内,说明 isEmpty==false,那么表示有数据了,不能生产,必须要等待消费者消费
                this.wait();//导致当前线程等待,进入等待池中,只能被其他线程唤醒
            }
             
            //-------生产数据开始-------
            this.name = name;
            //延时代码
            Thread.sleep(10);
            this.age = age;
            //-------生产数据结束-------
            isEmpty = false;//设置 isEmpty 为 false,表示已经有数据了
            this.notifyAll();//生产完毕,唤醒所有消费者
        } catch (Exception e) {
            e.printStackTrace();
        }
         
    }
    /**
     * 取数据,消费数据
     * @return
     */
    public synchronized void pop(){
        try {
            //不能用 if,因为可能有多个线程
            while(isEmpty){//进入 while 代码块,表示 isEmpty==true,表示为空,等待生产者生产数据,消费者要进入等待池中
                this.wait();//消费者线程等待
            }
            //-------消费开始-------
            Thread.sleep(10);
            System.out.println(this.name+"---"+this.age);
            //-------消费结束------
            isEmpty = true;//设置 isEmpty为true,表示需要生产者生产对象
            this.notifyAll();//消费完毕,唤醒所有生产者
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果:

Tom---11
Marry---21
Tom---11
Marry---21
Tom---11
Marry---21
Tom---11
......
Marry---21
Tom---11
Marry---21
Tom---11
Marry---21
Tom---11
Marry---21  

那么这便是我们期待的结果,交替出现。

 

死锁:

①、多线程通信的时候,很容易造成死锁,死锁无法解决,只能避免

②、当 A 线程等待由 B 线程持有的锁,而 B 线程正在等待由 A 线程持有的锁时发生死锁现象(比如A拿着铅笔,B拿着圆珠笔,A说你先给我圆珠笔,我就把铅笔给你,而B说你先给我铅笔,我就把圆珠笔给你,这就造成了死锁,A和B永远不能进行交换)

③、JVM 既不检测也不避免这种现象,所以程序员必须保证不能出现这样的情况

Thread 类中容易造成死锁的方法(这两个方法都已经过时了,不建议使用):

suspend():使正在运行的线程放弃 CPU,暂停运行(不释放锁)

resume():使暂停的线程恢复运行

情景:A 线程获得对象锁,正在执行一个同步方法,如果 B线程调用 A 线程的 suspend() 方法,此时A 暂停运行,放弃 CPU 资源,但是不放弃同步锁,那么B也不能获得锁,A又暂停,那么便造成死锁。

解决死锁法则:当多个线程需要访问 共同的资源A,B,C时,必须保证每一个线程按照一定的顺序去访问,比如都先访问A,然后B,最后C。就像我们这里的生产者---消费者模型,制定了必须生产者先生产一个对象,然后消费者去消费,消费完毕,生产者才能在开始生产,然后消费者在消费。这样的顺序便不会造成死锁。

 

线程的生命周期

线程是一个动态执行的过程,它也有从创建到死亡的过程。

在 Thread 类中,有一个枚举内部类:

  

上面的信息以图片表示如下:

  第一张图:

  

第二张图:把等待、计时等待、阻塞看成阻塞一个状态了

  

1、新建状态(new):使用 new 创建一个线程,仅仅只是在堆中分配了内存空间

           新建状态下,线程还没有调用 start()方法启动,只是存在一个线程对象而已

          Thread t = new Thread();//这就是t线程的新建状态

2、可运行状态(runnable):新建状态调用 start() 方法,进入可运行状态。而这个又分成两种状态,ready 和 running,分别表示就绪状态和运行状态

    就绪状态:线程对象调用了 start() 方法,等待 JVM 的调度,(此时该线程并没有运行)

    运行状态:线程对象获得 JVM 调度,如果存在多个 CPU,那么运行多个线程并行运行

  注意:线程对象只能调用一次 start() 方法,否则报错:illegaThreadStateExecptiong

3、阻塞状态(blocked):正在运行的线程因为某种原因放弃 CPU,暂时停止运行,就会进入阻塞状态。此时 JVM 不会给线程分配 CPU,知道线程重新进入就绪状态,才有机会转到 运行状态。

  注意:阻塞状态只能先进入就绪状态,不能直接进入运行状态

  阻塞状态分为两种情况:

    ①、当线程 A 处于可运行状态中,试图获取同步锁时,却被 B 线程获取,此时 JVM 把当前 A 线程放入锁池中,A线程进入阻塞状态

    ②、当线程处于运行状态时,发出了 IO 请求,此时进入阻塞状态

4、等待状态(waiting):等待状态只能被其他线程唤醒,此时使用的是无参数的 wait() 方法

  ①、当线程处于运行状态时,调用了 wait() 方法,此时 JVM 把该线程放入等待池中

5、计时等待(timed waiting):调用了带参数的 wait(long time)或 sleep(long time) 方法

  ①、当线程处于运行状态时,调用了带参数 wait 方法,此时 JVM 把该线程放入等待池中

  ②、当前线程调用了 sleep(long time) 方法

6、终止状态(terminated):通常称为死亡状态,表示线程终止

  ①、正常终止,执行完 run() 方法,正常结束

  ②、强制终止,如调用 stop() 方法或 destory() 方法

  ③、异常终止,执行过程中发生异常

下面详细介绍线程的几种方法:

  1、sleep(long millis)线程休眠:让执行的线程暂停一段时间,进入计时等待状态。

    static void sleep(long millis):调用此方法后,当前线程放弃 CPU 资源,在指定的时间内,sleep 所在的线程不会获得可运行的机会,此状态下的线程不会释放同步锁(注意和 wait() 的区别,wait 会放弃 CPU 资源,同时也会放弃 同步锁)

    该方法更多的是用来模拟网络延迟,让多线程并发访问同一资源时的错误效果更加明显。

   2、join()联合线程:表示这个线程等待另一个线程完成后(死亡)才执行,join 方法被调用之后,线程对象处于阻塞状态。写在哪个线程中,哪个线程阻塞

    这种也称为联合线程,就是说把当前线程和当前线程所在的线程联合成一个线程

class Join extends Thread{
    @Override
    public void run() {
        for(int i = 0 ; i < 10 ;i++){
            System.out.println("播放音乐"+i);
        }
    }
}
 
public class ThreadTest {
    public static void main(String[] args) {
        //创建 join 线程对象
        Join joinThread = new Join();
        for(int i = 0 ; i < 10 ; i++){
            System.out.println("玩游戏"+i);
            if(i==3){
                joinThread.start();
            }
            if(i==5){
                try {
                    joinThread.join();//强制运行 join 线程,知道 join 运行完毕了,main 才有机会运行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
 
}

结果:

玩游戏0
玩游戏1
玩游戏2
玩游戏3
玩游戏4
玩游戏5
播放音乐0
播放音乐1
播放音乐2
播放音乐3
播放音乐4
播放音乐5
播放音乐6
播放音乐7
播放音乐8
播放音乐9
玩游戏6
玩游戏7
玩游戏8
玩游戏9

 

后台线程(守护线程):在后台运行的线程,其目的是为其他线程提供服务,也称为“守护线程”。

①、JVM 的垃圾回收线程就是守护线程。

②、main 方法是前台线程,不是后台线程

  

public static void main(String[] args) {
        String mainThreadName = Thread.currentThread().getName();
        System.out.println(mainThreadName);  //main
         
        System.out.println(Thread.currentThread().isDaemon());//false
         
    }

特点:

①、若所有的前台线程都死亡,则后台线程自动死亡;

②、前台线程没有结束,后台线程是不会结束的;

③、前台线程创建的线程是前台线程,后台线程创建的线程是后台线程。

  Thread.setDaemon(Boolean on)必须在 start() 的方法前调用。否则会报错。

 

线程的优先级:

  每个线程都有一个优先级,这有助于 系统确定线程的调动顺序。

  Java 线程的优先级是一个整数,取值范围是:1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )

  默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

  具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

public class TestThread6 {
    public static void main(String args[]) {
        MyThread4 t4 = new MyThread4();
        MyThread5 t5 = new MyThread5();
        Thread t1 = new Thread(t4);
        Thread t2 = new Thread(t5);
        t1.setPriority(Thread.NORM_PRIORITY + 3);// 使用setPriority()方法设置线程的优先级别,这里把t1线程的优先级别进行设置
        /*
         * 把线程t1的优先级(priority)在正常优先级(NORM_PRIORITY)的基础上再提高3级 
         * 这样t1的执行一次的时间就会比t2的多很多     
         * 默认情况下NORM_PRIORITY的值为5
         */
        t1.start();
        t2.start();
        System.out.println("t1线程的优先级是:" + t1.getPriority());
        // 使用getPriority()方法取得线程的优先级别,打印出t1的优先级别为8
    }
}

class MyThread4 implements Runnable {
    public void run() {
        for (int i = 0; i <= 1000; i++) {
            System.out.println("T1:" + i);
        }
    }
}

class MyThread5 implements Runnable {
    public void run() {
        for (int i = 0; i <= 1000; i++) {
            System.out.println("===============T2:" + i);
        }
    }
}

run()方法一结束,线程也就结束了。

 

线程礼让:yield()方法:表示当前线程对象提示调度器自己愿意让出 CPU 资源,但是调度器可以自由的忽略该提示。

调用该方法后,线程对象进入就绪状态,所以完全有可能:某个线程调用了 yield() 方法,但是线程调度器又把它调度出来重新执行。

 package cn.galc.test;
 
 public class TestThread5 {
     public static void main(String args[]) {
         MyThread3 t1 = new MyThread3("t1");
         /* 同时开辟了两条子线程t1和t2,t1和t2执行的都是run()方法 */
         /* 这个程序的执行过程中总共有3个线程在并行执行,分别为子线程t1和t2以及主线程 */
         MyThread3 t2 = new MyThread3("t2");
         t1.start();// 启动子线程t1
         t2.start();// 启动子线程t2
         for (int i = 0; i <= 5; i++) {
             System.out.println("I am main Thread");
         }
     }
 }
 
 class MyThread3 extends Thread {
     MyThread3(String s) {
         super(s);
     }
 
     public void run() {
         for (int i = 1; i <= 5; i++) {
             System.out.println(getName() + ":" + i);
             if (i % 2 == 0) {
                 yield();// 当执行到i能被2整除时当前执行的线程就让出来让另一个在执行run()方法的线程来优先执行
                 /*
                  * 在程序的运行的过程中可以看到,
                  * 线程t1执行到(i%2==0)次时就会让出线程让t2线程来优先执行 
                  * 而线程t2执行到(i%2==0)次时也会让出线程给t1线程优先执行
                  */
             }
         }
     }
 }

运行结果如下:

从 Java7 提供的文档上可以清楚的看出,开发中会很少使用该方法,该方法主要运用于调试或测试,它可能有助于多线程竞争条件下的错误重现现象。

sleep() 和 yield() 方法的区别:

  ①、都能使当前处于运行状态的线程放弃 CPU资源,把运行的机会给其他线程

  ②、sleep 方法会给其他线程运行的机会,但是不考虑其他线程优先级的问题;yield 方法会优先给更高优先级的线程运行机会

  ③、调用 sleep 方法后,线程进入计时等待状态,调用 yield 方法后,线程进入就绪状态。

 

 

 

 

 

 

 

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wespten

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值