Java多线程-从基础到深入理解-01

Posted Frank Q

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程-从基础到深入理解-01相关的知识,希望对你有一定的参考价值。

本文主要记录了在Java多线程学习方面的一些主要内容,包括了多线程的理解,线程的生命周期,线程的创建三种方式,线程组,线程池,生产者消费者问题的实现以及售票等经典问题的一步一步的优化和实现过程;

1、线程与进程
2、线程的控制
3、线程中第二种创建方式
4、售票问题的优化与实现步骤


1、线程与进程(内容来自百度以及他人总结)

  • 线程与进程是最老生常谈的问题,所以将线程与进程相关内容总结如下:
  • 什么是进程?
    通过任务管理器我们就看到了进程的存在。
    而通过观察,我们发现只有运行的程序才会出现进程。
    进程:就是正在运行的程序。
    进程是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。
  • 多进程有什么意义呢?
    单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。
    举例:一边玩游戏(游戏进程),一边听音乐(音乐进程)。
    也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。
    并且呢,可以提高CPU的使用率。
  • 什么是线程呢?
    在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程。
    线程:是程序的执行单元,执行路径。是程序使用CPU的最基本单位。
    单线程:如果程序只有一条执行路径。
    多线程:如果程序有多条执行路径。
  • 多线程有什么意义呢?
    多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。
    程序的执行其实都是在抢CPU的资源,CPU的执行权。
    多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。
    我们是不敢保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。
  • 并行和并发:
    • 前者是逻辑上同时发生,指在某一个时间内同时运行多个程序。
    • 后者是物理上同时发生,指在某一个时间点同时运行多个程序。
  • Java程序运行原理:
    • 由JAVA命令启动JVM,JVM启动就相当于启动了一个进程
    • 接着有该进程创建了一个主线程去调用main方法
    • JVM虚拟机的启动的时候,加上主线程最低存在有两个线程,所以JVM必然是多线程

2、线程的控制(在下面的例子中将会主要以通过继承Thread最为演示例子为主)

  • 线程优先级的控制
    • 可以通过设置线程的优先级进行线程优先级的控制

ThreadPriority.java

public class ThreadPriority extends Thread 
    @Override
    public void run() 
        for (int i = 0; i < 100; i++) 
            System.out.println(this.getName() + "---" + i);
        
    

ThreadPriorityDemo.java

public class ThreadPriorityDemo 
    public static void main(String[] args) 
        ThreadPriority threadPriority1 = new ThreadPriority();
        ThreadPriority threadPriority2 = new ThreadPriority();
        ThreadPriority threadPriority3 = new ThreadPriority();

        threadPriority1.setName(">>>>thread1<<<<");
        threadPriority2.setName("====thread2====");
        threadPriority3.setName("==>>thread3<<==");

        //获取线程默认优先级(优先级:默认值5)
//      threadPriority1.getPriority();

        //设置线程优先级的方式
        threadPriority1.setPriority(1);
        threadPriority3.setPriority(10);

        threadPriority1.start();
        threadPriority2.start();
        threadPriority3.start();
    

  • 线程时机的控制
    • 可以通过设置线程的休眠时间进行控制(注意区分(sleep是线程中的方法,但是wait是属于Object中的方法这一点需要明确,之后会说明为什么会这样设计))

ThreadSleep.java

import java.util.Date;

public class ThreadSleep extends Thread 
    @Override
    public void run() 
        for (int i = 0; i < 100; i++) 
            System.out.println(getName() + "1" + i + "日期" + new Date());
            // 睡眠
            // 休息1秒钟
            try 
                Thread.sleep(1000);
             catch (InterruptedException e) 
                // TODO Auto-generated catch block
                e.printStackTrace();
            
        
    

ThreadSleepDemo.java

/*
 * 线程休眠
 *      public static void sleep(long millis)
 */
public class ThreadSleepDemo 
    public static void main(String[] args) 
        ThreadSleep ts1 = new ThreadSleep();
        ThreadSleep ts2 = new ThreadSleep();
        ThreadSleep ts3 = new ThreadSleep();

        ts1.setName("thread-demo01");
        ts2.setName("thread-demo02");
        ts3.setName("thread-demo03");

        ts1.start();
        ts2.start();
        ts3.start();
    
  • 停止线程
    • 可以通过使用stop停止线程

ThreadStop.java

import java.util.Date;

public class ThreadStop extends Thread 
    @Override
    public void run() 
        System.out.println("线程开始" + new Date());

        // 休息10秒钟
        try 
            Thread.sleep(10000);
         catch (InterruptedException e) 
            // e.printStackTrace();
            System.out.println("线程终止");
        

        System.out.println("结束执行" + new Date());
    

ThreadStopDemo.java

/*
 * public final void stop():
 * public void interrupt():终端线程,把线程状态终止,并且抛出异常
 */
public class ThreadStopDemo 
    public static void main(String[] args) 
        ThreadStop ts = new ThreadStop();
        ts.start();
        try 
            Thread.sleep(3000);
            //线程如果三秒钟没有醒来,就会被彻底终止
            // ts.stop();
            ts.interrupt(); //线程被打断 (就是为类抛出一个异常)
         catch (InterruptedException e) 
            System.out.println("====interrupt====");
        
    
  • 让某些线程在必须在执行完某个线程后才可以运行,类似于父子关系
    • 可以通过使用join函数控制线程

ThreadJoin.java

public class ThreadJoin extends Thread 
    @Override
    public void run() 
        for (int x = 0; x < 100; x++) 
            System.out.println(getName() + ":" + x);
        
    

ThreadJoinDemo.java

public class ThreadJoinDemo 

    public static void main(String[] args) 
        ThreadJoin tj1 = new ThreadJoin();
        ThreadJoin tj2 = new ThreadJoin();
        ThreadJoin tj3 = new ThreadJoin();

        tj1.setName("李渊");
        tj2.setName("李世民");
        tj3.setName("李元霸");

        tj1.start();
        try 
            // 等待该线程终止,必须要在此线程执行完毕,才可以执行
            // 为了保证某些线程执行完毕
            tj1.join();
         catch (InterruptedException e) 
            e.printStackTrace();
        

        tj2.start();
        tj3.start();
    
  • 让线程执行分配更加合适和谐
    • 可以通过使用yield函数控制线程

ThreadYield.java

public class ThreadYield extends Thread 
    @Override
    public void run() 
        for (int x = 0; x < 100; x++) 
            System.out.println(getName() + ":" + x);
            //线程礼让,暂停当前线程
            //保证线程执行更加和谐
            Thread.yield();
        
    

ThreadYieldDemo.java

public class ThreadYieldDemo 
    public static void main(String[] args) 
        ThreadYield ty1 = new ThreadYield();
        ThreadYield ty2 = new ThreadYield();

        ty1.setName("<<<thread-01>>>");
        ty2.setName(">>>thread-02<<<");

        ty1.start();
        ty2.start();
    
  • 设置守护线程

ThreadDaemon.java

public class ThreadDaemon extends Thread 
    @Override
    public void run() 
        for (int x = 0; x < 100; x++) 
            System.out.println(getName() + ":" + x);
        
    

ThreadDaemonDemo.java

public class ThreadDaemonDemo 
    public static void main(String[] args) 
        ThreadDaemon td1 = new ThreadDaemon();
        ThreadDaemon td2 = new ThreadDaemon();

        td1.setName("树叶");
        td2.setName("树叶");

        //守护线程
        td1.setDaemon(true);
        td2.setDaemon(true);

        td1.start();
        td2.start();

        //当当前运行的是守护线程的时候,Java虚拟机退出
        Thread.currentThread().setName("树根");
        for (int x = 0; x < 5; x++) 
            System.out.println(Thread.currentThread().getName() + ":" + x);
        
    

3、线程中第二种创建方式


MyRunnable.java

 public class MyRunnable implements Runnable 

    @Override
    public void run() 
        for (int i = 0; i < 100; i++) 
            // 实现接口的方式那名字的方法不同的地方,但是可以间接的使用
            System.out.println(Thread.currentThread().getName() + ":" + i);
        
    

MyRunnableDemo.java

/**
 * 实现Runnable接口
 * 步骤:
 *      A:自定义类MyRunnable实现Runnable接口
 *      B:重写run()方法
 *      C:创建MyRunnable类的对象
 *      D:创建Thread类的对象,并把C步骤的对象作为构造函数传递
 * @author YQ
 *
 */
public class MyRunnableDemo 

    public static void main(String[] args) 
        // 创建MyRunnable类的对象
        MyRunnable my = new MyRunnable();
        // 创建Thread类的对象,并且把C的步骤的对象作为构造函数参数传递
//      Thread thread1 = new Thread(my);
//      Thread thread2 = new Thread(my);

        Thread thread1 = new Thread(my,"线程一");
        Thread thread2 = new Thread(my,"线程二");      

        thread1.start();
        thread2.start();
    

线程生命周期详细图解:


4、售票问题的优化与实现步骤
* 下文中会对于三个窗口的售票问题进行模拟,会一步一步进行优化,最后给出一个可行版本;
版本一:

 public class SellTicket extends Thread 

    //为了保证多个线程共享这一百张票
    private static int tickets = 100;

    @Override
    public void run() 

        // 定义100张票
        // 每个线程对象都走这个位置,这样的话,相当于每个线程买的的是自己的票
//      int tickets = 100;
        while (true) 
            if (tickets > 0) 
                System.out.println(getName() + "正在出售票" + (tickets--) + "张票"); 
            
        

    
/**
 * 一共有100张票,而现在有3个窗口在进行售票,请设计一个程序模拟该电影院的售票
 * @author YQ
 *
 */
public class SellTicketDemo 

    public static void main(String[] args) 
        // 创建三个线程对象
        SellTicket st1 = new SellTicket();
        SellTicket st2 = new SellTicket();
        SellTicket st3 = new SellTicket();

        // 给每一个线程对象起名字
        st1.setName("窗口1");
        st2.setName("窗口2");
        st3.setName("窗口2");

        // 启动线程
        st1.start();
        st2.start();
        st3.start();
    


  • 版本一中存在的问题,因为是继承自Thread,所以需要开辟三个线程对象空间;

  • 版本二:
public class SellTicket implements Runnable 

    // 定义100张票
    private int tickets = 100;

    @Override
    public void run() 
        while (true) 
            if (tickets > 0) 
                System.out.println(Thread.currentThread().getName() + "正在出售第" 
                        + (tickets--) + "张票");
             else 
                break;
            
        
    

public class SellTicketDemo 

    public static void main(String[] args) 
        // 创建资源对象
        SellTicket st = new SellTicket();

        // 创建三个线程对象
        Thread thread1 = new Thread(st, "窗口1");
        Thread thread2 = new Thread(st, "窗口2");
        Thread thread3 = new Thread(st, "窗口3");

        // 启动线程
        thread1.start();
        thread2.start();
        thread3.start();
    

   

版本二中存在的问题是:没有真实的模拟出售票的一个时间等待的过程;


  • 版本三:
public class SellTicket implements Runnable 

    // 定义100张票
    private int tickets = 100;

    @Override
    public void run() 
        while (true) 
            if (tickets > 0) 
                // 为了模拟更加真实的场景,我们需要稍作休息
                // 自从加入了延迟之后,相同的票出现了多次
                try 
                    Thread.sleep(1000); // 当前线程稍作休息
                 catch (InterruptedException e) 
                    e.printStackTrace();
                

                System.out.println(Thread.currentThread().getName() + "正在出售第" 
                        + (tickets--) + "张票");
                // CPU的每一执行必需是不能够被再拆分的!
                // ticket-- : 先记录以前的值,在输出以前的值,在进行--
             else 
                break;
            
        
    

public class SellTicketDemo 

    public static void main(String[] args) 
        // 创建资源对象
        SellTicket st = new SellTicket();

        // 创建三个线程对象
        Thread thread1 = new Thread(st, "窗口1");
        Thread thread2 = new Thread(st, "窗口2");
        Thread thread3 = new Thread(st, "窗口3");

        // 启动线程
        thread1.start();
        thread2.start();
        thread3.start();
    

   

版本三中存在的问题是:没有实现同步过程,造成了一张票卖了多次甚至出现了负数购票的情况!


  • 版本四:(在实现了Runnable的接口类中加入共同的同步锁)
public class SellTicket implements Runnable 
    // 定义100张票
    private int tickets = 10;
    // 创建三个线程的相同的锁对象
    private Object obj = new Object();

    @Override
    public void run() 
        while (true) 
            synchronized(obj) 
                if (tickets > 0) 
                    // 为了模拟更加真实的场景,我们需要稍作休息
                    // 自从加入了延迟之后,相同的票出现了多次
                    try 
                        Thread.sleep(1000); // 当前线程稍作休息
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                    System.out.println(Thread.currentThread().getName() + "正在出售第" 
                            + (tickets--) + "张票");
                    // CPU的每一执行必需是不能够被再拆分的!
                    // ticket-- : 先记录以前的值,在输出以前的值,在进行--
                 else 
                    break;
                
            
        
    
public class SellTicketDemo 

    public static void main(String[] args) 
        // 创建资源对象
        SellTicket st = new SellTicket();

        // 创建三个线程对象
        Thread thread1 = new Thread(st, "窗口1");
        Thread thread2 = new Thread(st, "窗口2");
        Thread thread3 = new Thread(st, "窗口3");

        // 启动线程
        thread1.start();
        thread2.start();
        thread3.start();
    

   

版本四种存在的问题:虽然买票正常并且买票的过程具有一定的等待,但是可以看见出现了一个线程要独占很久才会进行释放的情况


版本五(比较好的版本):

public class SellTicket implements Runnable 

    // 定义100张票
    private static int tickets = 100;

    private int x = 0;

    @Override
    public void run() 
        while (true) 
            if (x%2==0) 
                synchronized(SellTicket.class)  // 会出现t1进来后,就将门封闭了,就只有t1现在才具有执行权利了
                    if (tickets > 0) 
                        // 为了模拟更加真实的场景,我们需要稍作休息
                        // 自从加入了延迟之后,相同的票出现了多次
                        try 
                            Thread.sleep(500); // 当前线程稍作休息
                         catch (InterruptedException e) 
                            e.printStackTrace();
                        

                        System.out.println(Thread.currentThread().getName() + "正在出售第" 
                                + (tickets--) + "张票");
                        // CPU的每一执行必需是不能够被再拆分的!
                        // ticket-- : 先记录以前的值,在输出以前的值,在进行--
                     else 
                        break;
                    
                
             else 
                sellTicket();
            

        
    

    private static synchronized void sellTicket() 
        if (tickets > 0) 
            // 为了模拟更加真实的场景,我们需要稍作休息
            // 自从加入了延迟之后,相同的票出现了多次
            try 
                Thread.sleep(1000); // 当前线程稍作休息
             catch (InterruptedException e) 
                e.printStackTrace();
            

            System.out.println(Thread.currentThread().getName() + "正在出售第" 
                    + (tickets--) + "张票");
            // CPU的每一执行必需是不能够被再拆分的!
            // ticket-- : 先记录以前的值,在输出以前的值,在进行--
         else 
            return;
        
    

总结:
同步解决线程安全问题

  • 1:同步代码块
    synchronized(对象)
    需要被同步的代码;

    这里的锁对象可以是任意对象。

  • 2:同步方法
    把同步加在方法上。这里的锁对象是this

  • 3:静态同步方法
    把同步加在方法上。
    这里的锁对象是当前类的字节码文件对象(反射再讲字节码文件对象)

以上是关于Java多线程-从基础到深入理解-01的主要内容,如果未能解决你的问题,请参考以下文章

深入理解 Java 多线程核心知识:跳槽面试必备

深入理解多线程—— Java虚拟机的锁优化技术

深入理解Java之多线程

深入理解多线程—— Java的对象头

互联网架构多线程并发编程高级教程(上)

深入理解多线程—— Java的对象模型