18 多线程

130 阅读5分钟

1 多线程

程序:数据结构+算法,主要指存放在硬盘上的可执行文件

进程:运行在内存中的可执行文件

进程是重量级的,每新建一个进程会消耗CPU和内存等系统资源,因此进程的数量比较局限。为了解决这个问题,提出线程的概念,所谓线程就是进程内部的程序流,操作系统内部支持多进程,而进程的内部有支持多线程,线程事情量级的,线程不会在消耗系统资源,只会共享所在进程的系统资源,这正是多线程的优势所在,目前主流的开发都使用多线程

多线程采用时间片轮换法来保证多个线程的并发,所谓并发是指宏观上并行微观上依旧是串行的运行机制。

2 多线程的创建

Thread类代表线程,任何线程对象都是Thread类的实例,是线程的模版,封装了复杂的线程开启等操作,封装了操作系统的差异性,其常用方法:

常用方法功能介绍
Thread()无参方式构造对象
Thread(Runnable target)根据参数引用构造对象
Thread(Runnable target,String name)根据参数引用和名称构造对象
Thread(String name)根据名称构造对象
void run()若使用Runnable引用构造线程对象,调用该方法时最终调用接口中的版本
若没有使用Runnable引用构造线程对象,调用该方法时则不执行任何操作
void start()启动线程,jvm虚拟机会自动调用线程的run方法

多线程的创建具体代码:

1 自定义类继承Thread类并重写run方法,然后创建该类的对象调用start方法

public class ThreadCreat extends Thread {
    @Override
    public void run() {
        for (int i=1;i<10;i++){
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
        ThreadCreat threadCreat=new ThreadCreat();
        threadCreat.start();
    }
}

2 实现Runnable接口并重写run方法

public class ThreadCreat implements Runnable {

    public static void main(String[] args) {
        ThreadCreat threadCreat=new ThreadCreat();
        Thread th=new Thread(threadCreat);
        th.start();

    }

    @Override
    public void run() {
        for (int i=1;i<10;i++){
            System.out.println(i);
        }
    }
}

3 匿名内部类的方式

public class ThreadCreat {

    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                for(int i=0;i<10;i++){
                    System.out.println(i);
                }
            }
        }.start();

    }
}

从上面三种创建线程的方式可以看出:继承Thread类的方式代码简单,但是由于继承了Thread类,就不能在继承其他类了;而实现Runnable接口的方式代码复杂,但不会影响该类继续继承其他类或实现其他接口,因此更加推荐实现Runnable接口方式。

3 线程的执行流程以及生命周期

线程的执行流程是:

  1. 执行main方法的线程叫做主线程,执行run方法的线程叫做子线程。
  2. main方法是程序的入口,对于start方法之前的代码来说,由主线程执行一次,当start方法调用成功后线程的个数由1个变成2个,新启动的线程去执行run方法的代码,主线程继续向下执行,两个线程各自独立运行互不影响
  3. 当run方法执行完毕后子线程结束,当mian方法结束后主线程结束
  4. 注意:两个线程执行没有明确的先后执行次序,由操作系统的调度算法决定

线程有五种状态,其生命周期如下:

Thread.PNG

  • 新建状态:使用new关键字创建之后进入的状态,此时线程还未执行

  • 就绪状态:调用start方法之后的状态,此时线程还未执行,在等待系统调度

  • 运行状态:线程调度器调用该线程后的状态,这时线程开始运行,当线程的时间片执行完毕,但任务还没有完成,就进入就绪状态

  • 消亡状态:当线程的任务执行完毕后就进入消亡状态,此时线程终止

  • 阻塞状态:当线程执行的过程中发生阻塞事件,线程就会进入阻塞状态,当阻塞状态解除后,就进入就绪状态

4 多线程的相关属性和方法

线程是具有编号和名称的,通过下面的方法就可以得到多线程的相关属性:

方法声明功能介绍
long getId()返回线程的编号
String getName()返回线程的名称
void setName(String name)设置线程的名称为参数指定名称
static Thread currentThread()获取当前正在执行的线程的引用

案例:自定义类继承Thread类并重写run方法,在run方法中先打印当前线程的名称和编号,然后将线程的名称修改为"张飞"后再次打印编号和名称(要求在main方法中也要打印主线程的编号和名称)

public class SubThread extends Thread {
    @Override
    public void run() {
        Thread th=Thread.currentThread();
        System.out.println("当前线程的编号是:"+th.getId());
        System.out.println("当前线程的名称是:"+th.getName());
        th.setName("zhangfei");
        System.out.println("当前线程的编号是:"+th.getId());
        System.out.println("当前线程的名称是:"+th.getName());
    }

    public static void main(String[] args) {
        Thread th=Thread.currentThread();
        System.out.println("主线程的编号是:"+th.getId());
        System.out.println("主线程的名称是:"+th.getName());
        SubThread st=new SubThread();
        st.start();

    }
}

Thread类常用方法:

方法声明功能介绍
static void yield()当前线程让出处理器,进入就绪状态等待
static void sleep(int times)当前线程从运行态进入阻塞状态,休眠times毫秒,再返回到就绪态
int getPriority()获取线程的优先级
void setPriority(int priority)设置线程的优先级
void join()等待该线程终止
void join(long mills)等待参数指定的毫秒数
boolean isDaemon()判断线程是否为守护线程
void setDaemon(boolean on)设置线程为守护线程

案例:创建两个线程,线程一负责打印1~100之间的所有奇数,其中线程二负责打印1~100之间的所有偶数,在main方法启动上述两个线程同时执行,主线程等待两个线程终止

public class ThreadOne extends Thread{
    @Override
    public void run() {
        for (int i=1;i<=100;i+=2){
            System.out.println("子线程一中: i= "+i);
        }
    }
}
public class ThreadTwo extends Thread{
    @Override
    public void run() {
        for(int i=2;i<=100;i+=2){
            System.out.println("------子线程二中:i = "+i);
        }
    }
}

public class ThreatTest {
    public static void main(String[] args) {
        ThreadOne threadOne=new ThreadOne();
        ThreadTwo threadTwo=new ThreadTwo();
        threadOne.start();
        threadTwo.start();
        System.out.println("主线程开始等待....");
        try {
            threadOne.join();
            threadTwo.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程等待结束");

    }
}

5 线程同步机制

当多个线程同时访问一个共享资源时,会出现数据的覆盖不一致的问题,此时就需要对线程之间进行通信和协调,该机制叫做线程同步机制,并且多个线程并发读写同一个临界资源会发生线程并发安全问题

线程有同步操作和异步操作,其中同步操作是指多个线程串行运行,有先后的执行顺序;异步操作是指多个线程各自独立运行,没有先后顺序。

解决线程并发安全问题的方案:当一个线程执行完毕后,再让另一个线程执行,将线程的并发操作改为串行操作,这样就可以避免并发安全问题

具体实现方式:在java中使用synchronized关键字来实现同步/对象锁机制从而保证线程执行的原子性,语法格式如下:

  • 使用同步代码块的方式实现部分代码锁定,格式:

    ​ synchronized(类类型的引用){

    ​ 编写所有需要锁定的代码;

    ​ }

  • 使用同步方法的方式实现所有代码的锁定,格式:

    直接使用synchronized关键字来修饰整个方法即可,等价于synchronized(this) {整个方法代码块};

静态方法的锁定:

  • 对一个静态方法加锁:public synchronized static void xxx() {.....}

  • 该方法的锁的对象是类对象,每个类只有一个类对象,获取类对象的方式:类名.class

  • 静态方法与非静态方法同时使用synchronized后它们之间是非互斥关系,原因是:静态方法锁的是类对象而非静态方法锁的是当前方法所属对象

使用synchronized保证线程同步的注意事项:

  • 多个需要同步的线程在访问同步块时,看到的应该是同一个锁对象引用
  • 使用同步块时应尽量减少同步范围以提高并发的效率

使用线程同步机制的实例代码:

public class AcountRunnableTest implements Runnable {
    private int balance;
    private demon d=new demon();

    public AcountRunnableTest() {
    }

    public AcountRunnableTest(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    @Override
    public void run() {
        System.out.println("线程"+Thread.currentThread().getName()+"已启动");
        synchronized (d) {
            int temp = getBalance();
            if (temp >= 200) {
                System.out.println("正在出钞,请稍等");
                temp -= 200;
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("请取走你的钞票");
            } else {
                System.out.println("余额不足,请换卡");
            }
            setBalance(temp);
        }

    }

    public static void main(String[] args) {
        AcountRunnableTest a1=new AcountRunnableTest(1000);
        Thread t=new Thread(a1);
        Thread t2=new Thread(a1);
        t.start();
        t2.start();
        System.out.println("主线程开始等待");
        try {
            t.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最终账户余额为:"+a1.getBalance());
    }

}
class demon{}

6 线程安全类和不安全类、死锁的概念

线程安全类和不安全类:

  • StringBuffer类是线程安全的类,但StringBuilder类不是线程安全的类
  • Vector类和Hashtable类是线程安全的类,但ArrayList类和HashTable类不是线程安全的类
  • Collections.synchronizedList()和Collections.synchronizedMap()等方法实现安全

死锁的概念:

线程一执行代码:

​ public void run(){

​ synchronized(a){ //持有对象锁a,等待对象锁b

​ synchronized(b){

​ 编写锁定的代码;

​ }

​ }

​ }

线程二执行的代码:

​ public void run(){

​ synchronized(b){ //持有对象锁b,等待对象锁a

​ synchronized(a){

​ 编写锁定的代码;

​ }

​ }

​ }

上面的代码就会造成死锁,线程一是持有对象锁a,但一直等待对象锁b,而线程二是持有对象锁b,一直等待对象锁a,这就会造成死锁,所以在开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用

7 Lock(锁)实现线程同步

java5开始提供了更强大的线程同步机制,即使用显示定义的同步锁对象来实现

Java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具,该接口主要实现类是ReentrantLock类,该类拥有与synchred相同的并发性,在以后的线程安全控制中,经常使用ReentrantLock类现实的加锁和释放锁

ReentrantLock类的常用方法:

方法声明功能介绍
ReentrantLock()无参方式构造对象
void lock()获取锁
void unlock()释放锁

实例代码:

import java.util.concurrent.locks.ReentrantLock;

public class AcountRunnableTest implements Runnable {
    private int balance;
    private demon d=new demon();
    private ReentrantLock lock=new ReentrantLock();

    public AcountRunnableTest() {
    }

    public AcountRunnableTest(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    @Override
    public void run() {
        lock.lock();
        System.out.println("线程"+Thread.currentThread().getName()+"已启动");

            int temp = getBalance();
            if (temp >= 200) {
                System.out.println("正在出钞,请稍等");
                temp -= 200;
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("请取走你的钞票");
            } else {
                System.out.println("余额不足,请换卡");
            }
            setBalance(temp);
            lock.unlock();

    }

    public static void main(String[] args) {
        AcountRunnableTest a1=new AcountRunnableTest(1000);
        Thread t=new Thread(a1);
        Thread t2=new Thread(a1);
        t.start();
        t2.start();
        System.out.println("主线程开始等待");
        try {
            t.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最终账户余额为:"+a1.getBalance());
    }

}
class demon{}

与synchronized 方式的比较:

  • Lock是显示锁,需要手动实现开启和关闭,而synchronized是隐式锁,执行锁定代码后自动释放
  • Lock只有同步代码块方式的锁,而synchronized有同步代码块方式和同步方法两种锁
  • 使用Lock锁方式时,java虚拟机将花费较少的时间来调度性能,因此性能更好

8 Object类常用方法

Object类的常用方法介绍:

方法声明功能介绍
void wait()使线程进入等待态,直到其他线程唤醒
void wait(long time)线程进入等待态,直到其他线程调用或参数指定的毫秒数过去
void notify()唤醒单个线程
void notifyAll()唤醒所有线程

实现两个线程之间的通信,具体实现代码如下:

public class ThreadCommunicate  implements Runnable{
    private int cnt;
    @Override
    public void run() {
        while (true){
            synchronized (this){
                notify();
                if(cnt<100){
                    System.out.println("线程"+Thread.currentThread().getName()+"中:cnt="+cnt);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    cnt++;
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else {
                    break;
                }
            }
        }

    }

    public static void main(String[] args) {
        ThreadCommunicate tc=new ThreadCommunicate();
        Thread thread=new Thread(tc);
        thread.start();
        Thread thread1=new Thread(tc);
        thread1.start();
    }
}

9 线程池

从java5开始增加创建线程的第三种方式是实现java.util.concurrent.Callable接口,接口常用方法:

方法声明功能介绍
V call()计算结果并返回

FutureTask类用于描述可取消的异步计算,该类提供了Future接口的基本实现,包括启动和取消计算、查询计算是否完成以及检索计算结果的方法,也可以用于获取方法调用后的返回结果,常用方法有:

方法声明功能介绍
FutureTask(Callable cal)根据参数引用创建一个未来任务
V get()获取call方法计算的结果

线程池的概念:创建一些线程,它们的集合称为线程池,服务器收到客户的请求后,就从线程池中取出一个空闲的线程为它服务,服务后不关闭该线程,而是将线程返还给线程池

线程池的原理:线程池的编程模式下,任务是提交给线程池,而不是交给某个线程,线程池收到任务后,会为任务分配一个空闲的线程为之服务,注意任务是提交给整个线程池的,而不是某个线程,一个线程同时只能服务一个任务,但可以同时向一个线程池提交多个任务。

线程池实例代码:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadCallableTest implements Callable {
    @Override
    public Object call() throws Exception {
        int sum=0;
        for(int i=1;i<=10000;i++){
            sum+=i;
        }
        System.out.println("计算结果是:"+sum);
        return sum;
    }

    public static void main(String[] args) {
        ThreadCallableTest tct=new ThreadCallableTest();
        FutureTask ft=new FutureTask(tct);
        Thread thread=new Thread(ft);
        thread.start();
        Object obj=null;
        try {
            obj=ft.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("线程处理结果的返回值是:"+obj);
    }
}

线程池相关的类和接口是:java5开始提供线程池的类和接口是Executors类和ExecutorService接口,关于这两个类的常用方法如下

Executors类是工具类和线程池的工厂类,可以创建并返回不同类型线程池,常用方法如下:

方法声明功能介绍
static ExecutorService newCachedThreadPool()创建一个可根据需要创建新线程的线程池
static ExecutorService newFixedThreadPool(int Threads)创建一个可重用固定线程数的线程池
static ExecutorService newSingleThreadExecutor()创建一个只有一个线程的线程池

ExecutorService接口是真正的线程池接口,主要实现ThreadPoolExecutor,常用方法如下:

方法声明功能介绍
void execute(Runnable command)执行任务和命令,通常用于执行Runnable
Future submit(Callable task)执行任务和命令,通常用于执行Callable
void shutdown()启动有序关闭

10 生产者消费者模型的实现

实现生产者消费者模型,具体实现代码如下:

仓库代码

public class StoreHouse {
    private int cnt=0;

    public synchronized void product(){
        notify();
        if(cnt<10){
            System.out.println("线程"+Thread.currentThread().getName()+"正在生产第"+(cnt+1)+"个产品");
            cnt++;
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void consumer(){
        notify();
        if(cnt>1){
            System.out.println("线程"+Thread.currentThread().getName()+"正在卖出第"+cnt+"个产品");
            cnt--;
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

生产者线程代码

public class ProductThread extends Thread{
    private StoreHouse st;

    public ProductThread(StoreHouse st) {
        this.st = st;
    }

    @Override
    public void run() {
        while (true){
            st.product();
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


    }
}

消费者代码

public class ConsumerThread extends Thread{
    private StoreHouse st;
    public ConsumerThread(StoreHouse st) {
        this.st = st;
    }

    @Override
    public void run() {
        while (true){
            st.consumer();
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

测试代码

public class StoreHouseTest {
    public static void main(String[] args) {
        StoreHouse st=new StoreHouse();
        ProductThread pt=new ProductThread(st);
        pt.start();
        ConsumerThread cs=new ConsumerThread(st);
        cs.start();


    }
}