Java多线程入门(绝对详细,多Demo帮助了解)

多线程入门

先大致了解下进程与线程
程序运行起来叫进程
进程包含若干线程(默认含有主线程、gc线程)

一、创建线程的三个方法:

  • 继承Thread类
package threadTest;

/**
 * 创建线程方式一
 * 继承Thread类
 * 重写run()方法
 * 调用start()方法启动线程
 */
public class ThreadTest extends Thread {
    public static void main(String[] args) {
        Thread thread = new ThreadTest();
        thread.start();
        for (int i=0;i<1000;i++){
            System.out.println("main线程运行中"+i);
        }
    }

    @Override
    public void run() {
        for (int i=0;i<100;i++){
            System.out.println("Thread子线程运行中"+i);
        }
    }
}
  • 实现Runnable接口
package threadTest;

/**
 * 创建线程方式二
 * 实现Runnable接口
 * 实现run()方法
 * 创建Thread类传入Runnable实现类对象
 * 调用Thread的start()方法
 */
public class RunnableTest implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("Runnable创建子线程运行中"+i);
        }
    }

    public static void main(String[] args) {

        Thread thread = new Thread(new RunnableTest());
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main线程运行中"+i);
        }
    }
}
  • 实现Callable接口(需要返回值类型,该方法目前仅做了解)
package threadTest;

import java.util.concurrent.*;

/**
 * 创建线程方式三
 * 实现Callable接口(拥有返回值)
 * 实现call方法,需要抛出异常
 * 创建Callable实现类对象
 * 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool()
 * 提交执行线程:Future result = ser.submit(new CallableTest)
 * 获取结果:boolean res = result.get();
 * 关闭服务:ser.shutdownNow();
 */
public class CallableTest implements Callable<Boolean> {
    @Override
    public Boolean call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println("Callable创建子线程运行中"+i);
        }
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableTest callableTest = new CallableTest();
        ExecutorService ser = Executors.newFixedThreadPool(1);
        Future<Boolean> result = ser.submit(callableTest);
        for (int i = 0; i < 1000; i++) {
            System.out.println("main线程运行中"+i);
        }
        boolean res = result.get();
        ser.shutdownNow();
    }
}

Thread方式与Runnable方式比较:

其一:

  • Runnable方式可以将程序代码与数据进行有效的分离
  • Thread方式则代码与数据具有较高的耦合性

其二:

  • Runnable方式可以避免由于Java单继承所带来的局限性
  • Thread只能够创建已继承的打单个Thread方式

二、Lambda表达式:

​ 函数式接口:任何接口如果只包含唯一一个抽象方法,那么它就是一个函数式接口

package threadTest;

/**
 * Lambda表达式
 * 前提条件需要一个函数式接口
 * 优点:避免内部类定义过多,简化代码
 * 仅留下核心逻辑
 */
public class LambdaTest {
    public static void main(String[] args) {
        LambdaInterface lambdaInterface;
        lambdaInterface =()->{
            System.out.println("Lambda表达式重写使用测试");
        };
        lambdaInterface.run();
    }
}
interface LambdaInterface{
    void run();
}
class LambdaImpl implements LambdaInterface{
    @Override
    public void run() {
        System.out.println("Lambda表达式使用测试");
    }
}

三、线程状态:

线程的生命周期和状态转换:

线程生命周期共有五个阶段:

  • 新建状态:新创建的对象所处状态,此时不能运行,但是JVM为其分配了内存,就和普通java对象一样
  • 就绪状态:线程对象调用start()方法后所处状态,此时可运行进入可运行池中,等待CPU调度
  • 运行状态:此时线程获取CPU使用权,开始执行run()方法
  • 阻塞状态:在某些特殊情况下会放弃CPU使用权,进入阻塞状态
    • 举例:
    • 当线程试图获取某个对象的同步锁时,如果锁被其他线程持有
    • 当线程调用阻塞式的IO方法时
    • 调用了某个对象的wait()方法
    • 调用了Thread的sleep()方法
    • 调用了另一个线程的join()方法
    • tip:线程只能从阻塞状态到就绪状态,不能直接进入运行状态
  • 死亡状态:线程run()方法执行完毕或抛出未捕获的Exception、错误Error时线程就会进入死亡状态。此时线程不可运行,也不可转换到其他状态

在这里插入图片描述

四、线程的调度:

在计算机中,线程调度有两种模型:分时调度模型和抢占式调度模型

分时调度:

线程轮流获取CPU使用权,平均分配每个线程占用的时间片

抢占式调度:

让可运行池中优先级较高的线程优先占用CPU,若优先级相同则随机选择。JVM默认采用抢占式调度

4.1 线程休眠

静态方法sleep(long millis):

  • 该方法可以让当前正在执行的线程暂停,进入休眠等待状态
  • 该方法抛出InterruptedException异常,使用时需要抛出或捕获
package ThreadMethodTest;

import threadTest.RunnableTest;

public class SleepTest implements Runnable{
    @Override
    public void run() {
        try {
            for (int i = 0; i < 100; i++) {
                if(i==50){
                        Thread.sleep(1000);
                }
                System.out.println("Runnable创建子线程运行中,50会休眠"+i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new SleepTest());
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main线程正运行中"+i);
        }
    }
}

4.2 线程让步:

静态方法yield():

将当前正在执行的前程暂停,转换为就绪状态,让CPU重新调度一次

package ThreadMethodTest;

public class YieldTest implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("Runnable创建的子线程在运行"+i);
            if(i==50){
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new YieldTest());
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main线程正在运行"+i);
        }
    }
}

4.3 线程插队:

join():

  • 在某个线程中调用其他线程的join()方法调用的线程将被阻塞,直到join()方法加入的线程执行完它才会执行
  • 需要抛出或处理异常InterruptedException
package ThreadMethodTest;

public class JoinTest implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //Thread.currentThread().getName()获取当前执行线程的名字
            System.out.println(Thread.currentThread().getName()+"正在执行中"+i);
            if(i==50){

            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new JoinTest());
        thread1.start();
        for (int i = 0; i < 100; i++) {
            if(i==50){
                thread1.join();
            }
            System.out.println("main线程执行"+i);
        }
    }
}

五、多线程同步:

多线程可以提高程序的效率,但是也会引发一些安全问题。例如:售票时,如果多个线程同时取同一张票,就可能导致错误。

引发错误的代码:

package synchronizedTest;


public class ErrorTest implements Runnable{
    private int tickets = 10;
    @Override
    public void run() {
        try {
            while (tickets>0){
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+"当前售出第"+(tickets--)+"张票");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ErrorTest errorTest = new ErrorTest();
        Thread thread1 = new Thread(errorTest,"售票员1");
        Thread thread2 = new Thread(errorTest,"售票员2");
        Thread thread3 = new Thread(errorTest,"售票员3");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

可以看出一张票可能被售出多次,甚至可能会出现票为负数的情况。这是因为当线程A正在取票,但是票的数量还未减1时,线程B也要取票,这样就导致取出了重复的票,显然这是不正确的,所以我们需要进行同步,来避免问题的出现

5.1 同步代码块:

保证共享资源在任何时刻都只能有一个线程访问,Java提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用synchronized关键字修饰,称为同步代码块

synchronized(lock){
    //操纵共享资源的代码块
}

lock是一个锁对象,它是同步代码块的关键。默认情况下lock为1,表示可以访问。如果当前有线程正在访问共享资源,则lock为0。不允许新的线程访问共享资源,使新线程进入阻塞状态。只有正在访问的线程离开后lock会重新置为1,允许访问。

对上面的错误代码进行修改:

package synchronizedTest;



public class ErrorTest_yes implements Runnable{
    private int tickets = 10;
    private Object lock = new Object();
    @Override
    public void run() {

        synchronized (lock){
            try {
                while (tickets>0) {
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName()
                            + "当前售出第" + (tickets--) + "张票");
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ErrorTest_yes yes = new ErrorTest_yes();
        Thread thread1 = new Thread(yes,"售票员A");
        Thread thread2 = new Thread(yes,"售票员B");
        Thread thread3 = new Thread(yes,"售票员C");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

tip:同步代码块中lock对象可以是任意类型的对象,但是多个线程共享的对象必须是唯一的。lock对象的创建不能放在run方法中,这样的话每一个线程都会拥有一个自己的锁,无法起到同步作用。

5.2 同步方法:

被synchronized修饰的方法就是同步方法,可以实现与同步代码块相同的功能

//synchronized 返回值 方法名([参数1,...]){}

使用同步方法修改上面的错误代码:

package synchronizedTest;



public class ErrorTest_yes implements Runnable{
    private int tickets = 10;
    private Object lock = new Object();
    @Override
    public void run() {
        saleTicket();
    }
    private synchronized void saleTicket(){
        try {
            while (tickets>0) {
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()
                        + "当前售出第" + (tickets--) + "张票");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ErrorTest_yes yes = new ErrorTest_yes();
        Thread thread1 = new Thread(yes,"售票员A");
        Thread thread2 = new Thread(yes,"售票员B");
        Thread thread3 = new Thread(yes,"售票员C");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}
  • 通过上面的代码大家可以看出也实现了同步。但是同步方法没有传入lock对象啊,他是怎么进行同步的呢?

答:其实同步方法的锁就是this对象。例如上代码,因为同步方法是被线程共享的,所以所有的线程都使用同一个yes对象,自然也就可以使用this来保证同步效果

  • 那么问题又来了?如果我们**用静态同步方法呢?**这时候是没有this的,他又是如何同步的呢?

答:静态同步方法的锁是静态方法所在的类的Class对象。因为在Java类加载机制中,类只被创建一次。所以也就可以被用来作为锁对象了

5.3 死锁问题:

有这样一个场景:一个中国人和一个美国人在一起吃饭,美国人拿了中国人的筷子,
中国人拿了美国人的刀叉,两个人开始争执不休: .
中国人:“你先给我筷子,我再给你刀叉!”
美国人:“你先给我刀叉,我再给你筷子!”

结果可想而知:两个人都吃不到饭。类似的问题还有哲学家就餐问题。有兴趣大家可以自行了解。此处不做赘述

代码模拟死锁问题:

package synchronizedTest;

public class DeadLock implements Runnable {
    private static Object chopsticks = new Object();   //筷子的锁
    private static Object knifeAndFork = new Object(); //刀叉的锁
    private boolean flag;       //flag带表是美国人还是中国人
    public DeadLock(boolean flag){
        this.flag = flag;
    }
    @Override
    public void run() {
        if(flag){   //当前说老美

                //筷子锁对象上的同步代码块
                synchronized (chopsticks){
                    System.out.println("把叉子给我,我就把筷子给你");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (knifeAndFork){    //开始伸手要刀叉
                        System.out.println("双标老美拿到刀叉");
                    }
                }

        }else{
                //刀叉对象的同步代码块
                synchronized (knifeAndFork) {
                    System.out.println("把筷子给我,我就把叉子给你");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (chopsticks) {
                        System.out.println("伟大的中国人民拿到筷子");
                    }
                }

        }
    }

    public static void main(String[] args) {
        DeadLock American = new DeadLock(true);
        DeadLock Chinese = new DeadLock(false);
        //创建并开启两个线程
        new Thread(American,"双标美").start();
        new Thread(Chinese,"博爱中").start();
    }
}

由上面代码可以看出双方互不松手,程序陷入死锁。所以在编程中我们需要避免死锁问题的发生

5.6 多线程通信

经典例子:生产者和消费者问题。

假设有一个场景:有一个仓库,生产者往里面放货物,消费者从里面取货物。如果仓库满了,如何让生产者停下后通知消费者取货。如果仓库空了,如何停止取货,让消费者通知生产者生产?

代码模拟一下:

package communicationTest;

/**
 * 定义一个仓库类
 */
public class Storage {
    //数据存储数组
    private int[] cells = new int[10];
    //inPos表示存入时数组下标,outPos表示取出时数组下标
    private int inPos;
    private int outPos;
    public void put(int num){
        cells[inPos] = num;
        System.out.println("在cells["+inPos+"]中放入数据--"+cells[inPos]);
        inPos++;
        //每当数据已经放满就从0位置重新开始放数据
        if(inPos == cells.length){
            inPos=0;    //当inPos为数组长度时,将其置为0
        }
    }
    //定义一个get方法从数组中取出数据
    public void get(){
        int data = cells[outPos];
        System.out.println("在cells["+outPos+"]中取出数据--"+cells[outPos]);
        outPos++;   //取完让元素位置++
        //每当数据已经取完就从0位置重新开始取数据
        if(outPos==cells.length){
            outPos=0;
        }
    }
}

生产者和消费者类:

package communicationTest;

/**
 * 生产者和消费者类
 * 生产者不断生产
 * 消费者不断消费
 */
class Input implements Runnable {
    private Storage st;
    private int flag=100;
    private int num;
    Input(Storage st){
        this.st = st;
    }
    @Override
    public void run() {
        while ((flag--)>0){
            st.put(num++);
        }
    }
}
class Output implements Runnable {
    private Storage st;
    private int flag=100;
    Output(Storage st){
        this.st = st;
    }
    @Override
    public void run() {
        while ((flag--)>0){
            st.get();
        }
    }
}
public class InputAndOutput{
    public static void main(String[] args) {
        //创建一个仓库对象
        Storage st = new Storage();
        Input input = new Input(st);
        Output output = new Output(st);
        new Thread(input).start();
        new Thread(output).start();
    }
}

根据运行结果能够发现,已经被放过数据还未被取出的位置又被重复放上数据,这是错误的。

那么如何解决问题呢?

此时就需要让线程之间彼此通信。Object类中提供了wait()、notify()、notifyAll()方法用于解决线程间的通信问题

  • wait():使当前线程放弃同步锁并进入等待,直到其他线程进入此同步锁,并调用notify()方法,或notifyAll()方法唤醒该线程为止
  • notify():唤醒此同步锁上等待的第一个调用wait()方法的线程
  • notifyAll():唤醒此同步锁上调用wait()方法的所有线程

注意:以上三个方法的调用者都应该是同步锁对象,如果不是则会抛出异常

对上面Storage代码的修改

package communicationTest;

/**
 * 定义一个仓库类
 */
public class Storage {
    //数据存储数组
    private int[] cells = new int[10];
    //inPos表示存入时数组下标,outPos表示取出时数组下标
    private int inPos;
    private int outPos;
    private int count;  //存入或取出数据的数量
    public synchronized void put(int num) {
        if(count==cells.length){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        cells[inPos] = num;
        System.out.println("在cells["+inPos+"]中放入数据--"+cells[inPos]);
        inPos++;
        count++;    //放入一个元素count++
        //如果已经放到最后一个位置,则从头开始放。(模拟循环队列)
        if (inPos==cells.length){
            inPos=0;
        }
        this.notify();
    }
    //定义一个get方法从数组中取出数据
    public synchronized void get() {
        if(count==0){   //如果已经全部取出
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        int data = cells[outPos];
        System.out.println("在cells["+outPos+"]中取出数据--"+data);
        cells[outPos]=-1;   //代表此处无元素
        outPos++;   //取完让元素位置++
        count--;
        if(outPos==cells.length){
            outPos=0;
        }
        this.notify();  //表示仓库已经可以放货物,通知生产者生产
    }
}

此时便不会出现重复放入元素或重复取出元素的情况

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章