多线程简介

一、概念

1、并发与并行

  • 并行多个任务在同一个时刻点同时执行。效率高
  • 并发多个任务在同一段时间内分时执行。效率低,宏观上同时执行,微观上分时执行。

2、进程与线程

  • 进程内存中正在运行的应用程序,是系统进行资源分配和调度的基本单位。每一个进程有自己独立的运行空间,相互之间不影响。进程就是程序的一次执行过程,即是一个进程从加载到内存到从内存中释放消亡的过程。
  • 线程进程内部的独立运行单元,是操作系统能够进行运算调度的最小单位,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

 

  • 多线程 在一个进程中,可以同时开启多个线程,让多个线程同时去执行某些任务(功能)。多线程的目的是提高程序的运行效率。
  • 主线程任何一个应用程序的运行,都有一个独立的运行入口。而负责这个入口的线程称为程序运行的主线程。Java程序的主线程即main线程。

 3、线程的调度

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

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

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

线程的调度方式分两类: 分时调度 抢占式调度(时间片轮转)

分时调度多个任务平均分配执行时间。

抢占式调度线程之间抢夺CUP的执行权,谁抢到谁执行(随机性)。


二、Thread 类

 java.lang.Thread: 示线程,是程序中执行的线程。 Java虚拟机允许应用程序同时执行多个执行线程。

1、多线程的创建 

创建一个新的执行线程有两种方法。

  • 创建子类,继承Thread类
  • 实现Runnable接口
  • 第一种方式
  1. 声明Thread的子类,声明一个类继承Thread
  2. 在子类中重写run()方法,即线程任务。
  3. 创建子类对象。
  4. 启动多线程。
  • 成员方法         
void run•() 线程任务方法,需要多线程执行的代码都写在此方法内。
void start•() 开启新的线程,java虚拟机调用此线程的run方法。  

声明 Thread 子类 

// 声明Thread子类
public class MyThread extends Thread {
    // 重写run()方法,线程任务
    @Override
    public void run() {
        for( int i = 1; i <= 20; i++ ) {
            System.out.println("旺财..." + i );
        }
    }
}

测试代码

public class Demo {
    public static void main(String[] args) {
        // 创建子类对象
        MyThread mt = new MyThread();
        // 启动线程任务
        // mt.run(); 直接调用run()方法无法开启新的线程
        mt.start(); // 开启新线程并调用run()方法
​
        for( int i = 1; i <= 20; i++ ) {
            System.out.println("小强...." + i );
        }
    }
}

调用run()方法 和 start()方法的区别

2、线程名

  • 多线程对象在创建出来之后,都有默认的线程名,命名规则: Thread-x , 从0开始,逐一增加。我们可以通过方法给线程命名,同样也可以获取线程的名字。
  • 构造方法给线程命名

Thread(String name) 分配新的 Thread 对象,并命名。

  • 成员方法给线程命名

void setName(String name) 将此线程的名称更改为等于参数 name   

  • 获取线程名

String getName() 返回此线程的名称。  

  • 获取线程对象

static Thread currentThread() 返回对当前正在执行的线程对象的引用。

线程代码演示

public class ThreadName extends Thread {
    // 构造方法
    public ThreadName() {
    }
    public ThreadName(String name) {
        super(name);
    }
​
    // 重写run()方法,线程任务
    @Override
    public void run() {
        for( int i = 1; i <= 20; i++ ) {
            System.out.println(getName() + ".." + i );
        }
    }
}

 测试代码演示

public class Demo {
    public static void main(String[] args) {
        // 创建线程对象一
        ThreadName tn1 = new ThreadName("旺财");
        // 获取线程的名字
        System.out.println( tn1.getName() );
​
        // 创建线程对象二
        ThreadName tn2 = new ThreadName();
        // 设置线程名字
        tn2.setName("来福..");
        // 获取线程的名字
        System.out.println(tn2.getName());
​
        // 启动线程
        tn1.start();
        tn2.start();
​
        // 主线程
        for( int i = 1; i <= 20; i++ ) {
            System.out.println(Thread.currentThread().getName() + i );
        }
    }
}

3、线程优先级

  • 每个线程都有优先级,具有较高优先级的线程优先于优先级较低的线程执行。线程的优先级范围是从 1- 10,默认的优先级是5

优先级成方法

  • int getPriority() 返回此线程的优先级。  
  • void setPriority(int newPriority) 更改此线程的优先级。  

 线程代码演示

public class ThreadPriority extends Thread {
    // 构造方法
    public ThreadPriority() {
    }
    public ThreadPriority(String name) {
        super(name);
    }
    // 线程任务
    @Override
    public void run() {
        for ( int i = 1; i <= 20; i++ ) {
            System.out.println( getName() + ".." + i );
        }
    }
}

 测试代码演示

public class Demo {
    public static void main(String[] args) {
        // 创建线程任务
        ThreadPriority tp1 = new ThreadPriority("tp1.");
        ThreadPriority tp2 = new ThreadPriority("tp2..");
        ThreadPriority tp3 = new ThreadPriority("tp3...");
        // 设置线程优先级  tp2为默认优先级
        tp1.setPriority(1);
        tp3.setPriority(10);
        // 获取优先级
        System.out.println( tp1.getPriority() );
        System.out.println( tp2.getPriority() );
        System.out.println( tp3.getPriority() );
        // 启动线程
        tp1.start();
        tp2.start();
        tp3.start();
    }
}

4、守护线程

API中有这样的描述,每个线程可能也可能不会被标记为守护程序,并且当且仅当创建线程是守护进程时才是守护线程。

线程分为用户线程和守护线程两种

  • 用户线程:也就是普通线程,刚创建出来的线程都属于用户线程。
  • 守护线程:顾名思义,守护线程是用来守护的,专门用于服务用户线程,当程序中没有正在运行的用户线程时,守护线程会自动结束。垃圾回收线程就是典型的守护线程。

守护线程方法

  • void setDaemon(boolean on) 将此线程标记为 daemon线程或用户线程。  
  • boolean isDaemon() 测试这个线程是否是守护线程。

 线程代码演示

public class ThreadDaemon extends Thread {
    // 线程任务
    @Override
    public void run() {
        for ( int i = 1; i <= 200; i++ ) {
            System.out.println( "守护线程.." + i );
        }
    }
}

 测试代码演示

public class Demo {
    public static void main(String[] args) {
        // 创建线程对象
        ThreadDaemon td = new ThreadDaemon();
        // 将线程设置为守护线程
        td.setDaemon(true);
        // 启动线程
        td.start();
​
        /*
            主线程循环
                此时主线程就是一个用户线程,主线程循环20次,
                而上面的守护线程循环200次,注意当主线程执行
                结束之后,守护线程的执行数据
          */
        for( int i = 1; i <=20;i++ ) {
            System.out.println("main.." + i );
        }
    }
}

5、线程休眠

休眠方法

  • static void sleep(long millis) 使当前正在执行的线程停留(暂停执行)指定的毫秒数,这取决于系统定时器和调度程序的精度和准确性。  

代码演示

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        // 使用线程休眠方法 制作一个闹钟
        for( int i = 1;i <= 10; i++ ) {
            Thread.sleep(1000);
            System.out.println("第" + i + "秒");
        }
        System.out.println("叮铃铃...");
    }
}

三、Runnable 接口

  • java.lang.Runnable: 线程任务接口,该接口应由任何类实现,其实例将由线程执行。 类必须定义一个无参数的方法,称为run 。

多线程的第二种方式

1. 声明Runnable接口的实现类

2. 实现类中重写run()方法

3. 创建实现类对象

4. 创建Thread线程对象,并将实现类对象传递给构造方法。

5. 启动线程

Runnable实现类

public class MyRunnable implements Runnable {
    // 线程任务
    @Override
    public void run() {
        for( int i = 1; i <= 20; i++ ) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}

测试代码演示

public class Demo {
    public static void main(String[] args) {
        // 创建线程任务对象
        MyRunnable mr = new MyRunnable();
        // 创建线程对象
        Thread thread1 = new Thread(mr);
        Thread thread2 = new Thread(mr);
        // 启动线程
        thread1.start();
        thread2.start();
    }
}

2、线程两种创建方式的区别

打破了Java的单继承性

  • Java具备单继承性,一个子类只能继承一个父类。
  • 多线程的第一种创建方式,需要继承Thread类。如果此时这个类已经继承了其他的父类,就无法继承Thread类。如果改为继承Thread类就改变了这个类的当前继承体系。
  • 多线程的第二种创建方式,不需要类采用继承的方式实现,在实现Runnable接口的同时不会影响到类原有的继承体系。

实现类解耦

  • 多线程的第一种创建方式,线程对象和线程任务是直接耦合在一起的。
  • 多线程的第二种创建方式,实现了线程对象和线程任务的解耦。线程对象专门用来对线程本身进行操作,线程任务单独抽取到Runnable接口中独立操作。当一个类实现了Runnable接口,就相当于有了线程任务,创建Thread对象拿到线程任务就可以执行,达到了线程任务和线程对象的分离及结合。

四、匿名线程

第一种方式实现匿名线程

public class Demo {
    public static void main(String[] args) {
        // 创建Thread匿名子类
        new Thread(){
            // 线程任务
            @Override
            public void run() {
                System.out.println("线程启动...");
            }
        }.start();
    }
}

第二种方式实现匿名线程

public class Demo {
    public static void main(String[] args) {
        // 创建Thread匿名子类 传递匿名Runnable对象
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程启动...");
            }
        }).start();
    }
}

练习:就近原则,离的近的调用方法

public class Demo {
    public static void main(String[] args) {
        // 运行哪段代码
        new Thread(new Runnable() {
            // 匿名Runnable对象
            @Override
            public void run() {
                System.out.println("Runnable匿名实现类..线程启动...");
            }
        }){
            // Thread匿名子类
            @Override
            public void run() {
                System.out.println("Thread匿名子类类..线程启动...");
            }
        }.start();
    }
}

5、线程安全

1、线程的安全分析

  • 有时候我们需要使用多线程操作共享的数据,在操作共享数据的过程中特别容易发生线程的安全问题。下面我们通过一个案例来看一下线程安全问题发生的原因。

案例

  • 使用多线程模拟火车站售票,每一条线程相当于一个售票的窗口。而所有窗口所售的票是共享的,也就是说当有一个窗口把某张票卖出去之后,其他的窗口也就不能在卖那张票。窗口可以有多个,但是每一张票是唯一的。

线程任务

public class Ticket implements Runnable {
    // 声明变量 模拟车票
    private int num = 100;
    // 线程任务就是售票
    @Override
    public void run() {
        // 死循环模拟窗口在一直售票
        while ( true ) {
            // 判断是否还有车票
            if( num > 0 ) {
                // 获取线程名字
                String name = Thread.currentThread().getName();
                System.out.println( name + "售票:" + num );
                // 线程休眠,模拟出票时间
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 票数迭代,减去售出的票
                num--;
            }
        }
    }
}

测试类

public class Demo {
    public static void main(String[] args) {
        // 创建线程任务对象
        Ticket ticket = new Ticket();
        // 创建线程对象 模拟售票窗口
        Thread t1 = new Thread(ticket,"窗口一");
        Thread t2 = new Thread(ticket,"窗口二");
        Thread t3 = new Thread(ticket,"窗口三");
        Thread t4 = new Thread(ticket,"窗口四");
        // 启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

运行结果

 原因分析

  • 通过程序的运行结果可以看到,有些票被多次卖出,而有些票没有被卖出而是被覆盖掉。发生问题的原因是多线程在执行售票任务的时候,操作了共享的成员变量num。可是在操作num的过程中,一个线程操作到其中一部分代码的时候,CPU切换到其他线程开始执行线程任务,这样就导致了num变量的值被修改的不一致。

类似于上述的这些问题,我们称为线程的安全问题。而造成多线程安全问题的原因

  1. 有多条线程
  2. 多个线程在操作共享的数据
  3. 操作共享数据的语句不止一条,并且对共享数据有修改
  • 本质的原因是CPU在处理多个线程的时候,在操作共享数据的多条代码之间切换导致的,一条线程的线程任务还没有直接结束,就切换到了另外一条线程。

解决方案

  • 上述的问题分析我们知道,造成安全的原因的CPU的随机切换造成的,但是CPU是由操作系统控制的,我们无法直接干预CPU的切换,所以只能从线程本身入手。

解决方案:我们可以人为的控制,当有一条线程在执行操作共享数据的代码时,不让其他线程进入到操作共享数据的代码中。只有当某条线程将操作共享数据的代码执行结束,其他线程才可以继续执行操作共享语句的代码。这样就可以保证线程的安全。

上述的解决方案,称为线程的同步。实现线程同步的方式有3种:

  1. synchronized同步代码块
  2. 同步方法
  3. Lock锁机制

2、同步代码块

语法:

synchronized (任意唯一锁对象) {
    操作共享数据的代码;
}
public class Ticket implements Runnable {
    // 声明变量 模拟车票
    private int num = 100;
    // 声明锁对象
    private Object lock = new Object();
    // 线程任务就是售票
    @Override
    public void run() {
        // 死循环模拟窗口在一直售票
        while ( true ) {
            // 同步代码块
            synchronized ( lock ) {
                // 判断是否还有车票
                if (num > 0) {
                    // 获取线程名字
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "售票:" + num);
                    // 线程休眠,模拟出票时间
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 票数迭代,减去售出的票
                    num--;
                }
            }
        }
    }
}

3、同步方法

        当在run()方法中调用了其他方法时,那么被调用的方法也间接变为了线程任务。

同步方法,就是使用synchronized关键字修饰的方法,当某个方法中的代码全部是操作共享数据的代码时,我们可以直接将当前方法声明为同步方法。

语法

public synchronized void methodName( 形参 ) {
    操作共享数据的代码;        
}
public class Ticket implements Runnable {
    // 声明变量 模拟车票
    private int num = 100;
    // 线程任务就是售票
    @Override
    public void run() {
        // 死循环模拟窗口在一直售票
        while ( true ) {
            ticket();
        }
    }
    // 声明一个专门售票的方法
    public synchronized void ticket() {
        // 判断是否还有车票
        if (num > 0) {
            // 获取线程名字
            String name = Thread.currentThread().getName();
            System.out.println(name + "售票:" + num);
            // 线程休眠,模拟出票时间
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 票数迭代,减去售出的票
            num--;
        }
    }
}

注意事项

  1. 只有方法中的所有代码都是操作共享数据的代码时,才使用synchronized声明为同步方法,否则会降低执行效率。
  2. run()方法不能使用synchronized修饰,否则线程任务无法被多线程执行。

4、继承实现售票案例

继承的方式不利于数据的共享,所以需要考虑的如何实现数据的共享问题。

public class Ticket extends Thread {
    // 声明变量 模拟车票 需要静态,保证车票被共享
    private static int num = 100;
    // 声明锁对象 需要静态,保证锁唯一
    private static Object lock = new Object();
    // 构造方法给线程命名
    public Ticket(String name) {
        super(name);
    }
​
    // 线程任务就是售票
    @Override
    public void run() {
        // 死循环模拟窗口在一直售票
        while ( true ) {
            // 同步代码块
            synchronized ( lock ) {
                // 判断是否还有车票
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + "售票:" + num);
                    // 模拟出票时间
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 迭代车票
                    num--;
                }
            }
        }
    }
}

测试类

public class Demo {
    public static void main(String[] args) {
        // 创建线程对象 模拟窗口
        Ticket ticket1 = new Ticket("窗口一");
        Ticket ticket2 = new Ticket("窗口二");
        Ticket ticket3 = new Ticket("窗口三");
        Ticket ticket4 = new Ticket("窗口四");
        // 启动线程
        ticket1.start();
        ticket2.start();
        ticket3.start();
        ticket4.start();
    }
}

5、同步的细节

当程序中加了同步之后,依然存在线程的安全问题,那么原因有如下两个

  1. 锁不唯一
  2. 同步代码块没有加在所有操作共享数据的代码上

同步的好处和弊端:

  1. 使用同步会影响程序的执行效率。每次CPU运行到同步代码时,都需要去判断有没有线程在同步代码中,如果只能等待同步代码中的线程执行结束。
  2. 好处显而易见是可以保证数据的安全。

在前面学习的:

  • StringBuffer它是线程安全的。提供的方法中有同步。只要有同步效率肯定会降低。
  • StringBuilder它是线程不安全的。

在集合中学习的JDK1.2出现的所以有集合都是线程不安全,JDK1.2之前的都是线程安全的。

VectorHashtable

如果开发时需要对集合进行线程的安全操作,这时需要使用Collections中的方法,把不安全的集合变成安全的集合。


六、单例懒汉式线程安全问题 

单例设计模式:程序在运行的过程中只允许产生一个对象。

单例设计模式的实现过程一共3步:

  1. 私有构造方法。
  2. 本类创建对象。
  3. 提供公开静态获取本类对象的方法。

代码的实现分为懒汉式饿汉式两种。

饿汉式

public class Single { 
   // 私有构造方法
    private Single(){
        
    }
    // 本类创建对象
    private static final Single s = new Single();
    // 返回本类对象的方法
    public static Single getInstance() {
        return s;
    }
}

懒汉式

public class Single {
   // 私有构造方法
    private Single(){}
    // 本类创建对象
    private static Single s = null;
    // 返回本类对象的方法
    public static Single getInstance() {
        if ( s == null ) {
            s = new Single();
        }
        return s; // 返回对象
    }
}
  • 上述的两种方式中,饿汉式的对象创建过程只有一条语句,不会发生线程的安全问题。但是懒汉式的对象创建不止一条语句,如果有多线程操作时,就会发生线程的安全问题。下面我们通过代码来验证一下,线程任务就是获取单例的对象,查看对象是否唯一。

线程任务

public class SingleThread implements Runnable {
    // 线程任务就是获取单例对象
    @Override
    public void run() {
        // 获取单例对象
        Single instance = Single.getInstance();
        // 打印对象
        System.out.println(instance);
    }
}

测试代码

public class Demo {
    public static void main(String[] args) {
        // 创建线程任务对象
        SingleThread st = new SingleThread();
        // 创建线程对象
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);
        Thread t3 = new Thread(st);
        Thread t4 = new Thread(st);
        // 启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

运行结果

  •  通过多线程获取单例对象时发现并没有实现对象唯一,原因很简单,就是创建对象的语句有多条,而我们没有实现线程的同步。

线程同步:两次判断,第一次判断提高效率,第二次用来保证线程安全

public class Single {
    // 声明锁对象
    private static Object lock = new Object();
   // 私有构造方法
    private Single(){}
    // 本类创建对象
    private static Single s = null;
    // 返回本类对象的方法
    public static Single getInstance() {
        /*
            外出的判断可以提高程序的运行效率,当有一条线程创建完对象之后
            其他线程就不会在进入到同步代码块中。
          */
        if( s == null ) {
            // 同步代码块
            synchronized (lock) {
                if (s == null) {
                    s = new Single();
                }
            }
        }
        // 返回对象
        return s;
    }
}


七、多线程的死锁问题 (避免)

  • 死锁:多个线程操作共享数据,但是要求线程获取锁的先后次序不同。但是都必须根据自己的次序获取所有的锁才能去执行这个任务。而在获取锁的过程中,不同的线程获取方式不同,导致锁会被其他的线程占有。一旦发生这个问题,就立刻发生死锁问题。

案例

  • 有2个线程,需要执行相同的任务,但是需要分别获取的A和B锁才能去执行,第一个线程获取锁的顺序是先A后B。第二个线程获取锁的顺序是先B后A。

线程任务演示

public class DieThread implements Runnable{
    // 声明AB两个锁对象
    private Object lock_A = new Object();
    private Object lock_B = new Object();
    // 声明变量 控制执行流程
    boolean flag = false;
    @Override
    public void run() {
        if( flag ) {
            while ( true ) {
                // 获取A锁的同步代码块
                synchronized (lock_A ) {
                    System.out.println("if..lock_A 锁...");
                    // 获取B锁的同步代码块
                    synchronized (lock_B) {
                        System.out.println("if..lock_B锁...");
                    }
                }
            }
        } else {
            while (true){
                // 获取B锁的同步代码
                synchronized (lock_B) {
                    System.out.println("else..lock_B锁...");
                    // 获取A锁的同步代码块
                    synchronized (lock_A) {
                        System.out.println("else..lock_A锁...");
                    }
                }
            }
        }
    }
}

测试演示

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        // 创建线程任务对象
        DieThread dt = new DieThread();
        // 创建线程对象
        Thread a = new Thread(dt);
        Thread b = new Thread(dt);
        // 启动线程任务
        a.start();
        // 线程休眠
        Thread.sleep(1);
        // 修改flag的值,让线程可以进入if语句
        dt.flag = true;
        // 启动线程
        b.start();
    }
}

 

  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值