关于Java中多线程

Posted 武帅祺的官方网站

tags:

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

基本概念

什么是进程-->是操作系统资源分配和调度的最小(基本)单位(操作系统分配给当前进程一个内存区域供其使用)

什么是线程-->是程序运行的基本单位(等待操作系统分配时间片 让CPU执行该内存区域中的代码)

进程和线程的关系-->一个进程可以存在多个线程 线程是由进程创建的(寄生在进程中) 线程是进程中一个负责程序执行的控制单元

什么是单线程-->只处理一个任务

什么是多线程-->宏观上同时处理多个任务即并发执行也就是说允许单个进程运行多个线程来完成不同的任务(站在CPU角度微观上其实1个CPU每时刻只能执行一个任务 但是宏观上看这CPU一会执行这个一会执行那个)

谈谈线程的几种状态-->开始(new)-->就绪(runnable)-->运行(runnint)-->阻塞(blocked)-->销毁(dead)

多线程有什么好处-->异步处理任务 将IO任务异步(阻塞IO线程让其他线程进入开始状态)操作提高CPU利用率

多线程有什么缺点-->线程也是程序当线程越来越多会导致占用内存大 多线程需要协同管理需要跟棕栈 线程之间对共享资源的访问必须解决竟用的问题 线程太多会导致程序过于复杂难以排除bug

一个java程序至少有几个线程-->当我们启动java程序的时候会立即执行main()方法这个线程被称为程序的主线程和垃圾回收线程

聊聊Java中的多线程

java.util.concurrent简称JUC

实现多线程的方式-->调用Thread.start()方法

  • 重写Runnable接口中的run()方法 实例化Runnable接口实现类并作为参数传递给Thread
  • 继承Thread类并重写run()方法 实例化继承类
  • 重写Callable接口中的call()方法产生的回调结果参数传递给FutureTask()构造函数然后将实例对象作为参数传递给Thread构造函数

怎么开启一个线程-->不管通过哪种方式实例化Thread的时候 线程就到开始状态了

什么是初始态-->仅仅在语言层面创建一个线程实例

怎么到就绪态-->调用Thread.start()方法 线程进入就绪态等待操作系统分配时间片

怎么到运行态-->操作系统分配时间片给当前线程

怎么从运行态回到就绪态-->时间片用完还没有完成任务那么就又回到就绪态

怎么从运行态到阻塞态-->进行耗时的IO操作时 操作系统拿到其时间片将其分配给其他线程 或者Thread.sleep()方式强制进入睡眠状态

怎么从阻塞态回到就绪态-->在阻塞态的线程完成其操作回到就绪队列中等待操作系统分配时间片

怎么从运行态到销毁-->线程中的代码执行完毕然后jvm收回线程占用的资源!

什么是主线程-->Main线程 垃圾回收线程

怎么让线程停止-->Thread类提供stop()方法用于停止一个已启动的线程但本质是不安全的

如何安全停止一个正在运行的线程-->线程对象在执行完run()方法所有代码执行完成后线程会自然消亡因此需要在运行过程提前停止线程 可以通过更改变量值的方法run()方法提前结束

创建线程的三种方式

public class MyThread extends Thread
    @Override
    public void run()
        System.out.println("操作系统分配时间片给我啦!");
    

public class MyRunnable implements Runnable
    @Override
    public void run() 
        System.out.println("操作系统分配时间片给我啦!");
    

public class MyCallable implements Callable<String> 
    @Override
    public String call() throws Exception 
        return "操作系统分配时间片给我!!";//产生一个返回结果
    

测试类

public class TestCreate 
    @Test
    public void test1() throws Exception
        MyRunnable myRunnable = new MyRunnable();
        //创建线程
        Thread t1=new Thread(myRunnable,"实现runnable接口的方式");
        MyThread t2=new MyThread("继承thread的方式");
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask=new FutureTask<>(myCallable);
        Thread t3 = new Thread(futureTask);//futureTask负责调用call()函数
        t3.setName("我是实现Callable的方式");
        t1.start();
        t2.start();
        t3.start();
        String s = futureTask.get();//通过get方式拿到返回的结果
        System.out.println("这里是返回的结果-->"+s);
    

线程中断

打断正在执行的线程

  • Thread实例.stop()方式

stop方式会杀死一个进程 如果此时该线程锁住了一些共享资源 那么它被杀死以后就再也没有机会释放锁 其他线程将永远无法获取锁

  • System.exit(int)方式停止线程

目的是停止一个线程 但这样会让整个程序都停止!

打断被堵塞的线程

  • 两阶段终止模式
public class MonitorThread
    //维护一个Thread实例 来监控是否被打断
    private Thread monitor;
    public void start()
        monitor=new Thread(()->
            while (true)
                //isInterrupted不会清除打断标记
                if (monitor.isInterrupted())
                    System.out.println("料理后事...");
                    break;
                
                try 
                    //每次5秒1记录
                    Thread.sleep(5);
                    System.out.println("监控记录");
                catch (InterruptedException e)
                    e.printStackTrace();
                    System.out.println(monitor.isInterrupted());//false
                    monitor.interrupt();//重置打断变量
                
            
        );
        monitor.start();
    
    //停止监控线程
    public void stop()
        monitor.interrupt();
    

    public static void main(String[] args) throws InterruptedException 
        MonitorThread monitorThread = new MonitorThread();
        monitorThread.start();
        Thread.sleep(100);
        monitorThread.stop();
    

守护线程

主线程执行完毕-->守护线程无论是否执行完毕就dead

守护线程-->垃圾回收器 tomcat中的分发器

public static void main(String[] args) throws InterruptedException 
    Thread t1=new Thread(()->
        while (true)
            System.out.println("我是daemon线程...");
    );
    //设置为守护线程
    t1.setDaemon(true);
    t1.start();//线程不可以多次执行start()方法
    Thread.sleep(10);
    System.out.println("主线程准备结束--守护线程也被挂掉了");

烧水喝茶

graph LR; a(洗水壶1分钟)-->b(煮水15分钟)-->c(泡茶叶) d(洗茶壶 拿茶叶 洗茶叶 4分钟)-->c
public static void main(String[] args) 
    //两个线程模拟烧水喝茶的过程
    Thread t1=new Thread(()->
        try 
            System.out.println("水壶清洗...");
            Thread.sleep(1000);
            System.out.println("烧水...");
            Thread.sleep(15000);
         catch (InterruptedException e) 
            throw new RuntimeException(e);
        
    );
    Thread t2=new Thread(()->
        try 
            System.out.println("洗茶壶");
            Thread.sleep(1000);
            System.out.println("拿茶叶");
            Thread.sleep(2000);
            System.out.println("洗茶叶");
            Thread.sleep(1000);
            t1.join();
        catch (InterruptedException e)
            e.printStackTrace();
        
        System.out.println("泡茶ok");
    );
    t1.start();//小王
    t2.start();//小张

共享带来的问题

一个程序运行多个线程本身是没有问题的 但问题出在多个线程访问共同的资源

//全局变量2个线程访问
public class Test3 
    static int global=0;
    public static void main(String[] args) 
        Thread t1=new Thread(()->
           for (int i=0;i<2500;i++)
               global++;
           
        );
        Thread t2=new Thread(()->
            for (int i=0;i<2500;i++)
                global--;
            
        );
        t1.start();
        t2.start();
        try 
            t1.join();
            t2.join();
         catch (InterruptedException e) 
            throw new RuntimeException(e);
        
        //为什么执行2500次g++和2500次g--结果并不一定是0 字节码角度分析
        // 自增自减运算符并非一条指令(非原子操作)
        System.out.println(global);
    

在一个线程中

临界区

Java——10个关于Java中多线程并发的面试题

1.多线程的创建方式有几种?

答案:应该是3种。

1.1 继承Thread类,重写run方法

/**
 * 实现线程的第一种方式: 直接继承java.lang.Thread,重写run方法
 * 怎么创建线程?  new就可以了
 * 怎么启动线程?  start就可以了
 */
class MyThread extends Thread {

    @Override
    public void run() {
        //这段程序运行在分支线程中(分支栈)
        for (int i=0;i<10;i++) {
            System.out.println("分支线程---> " + i);
        }
    }
}

public class ThreadTest02 {

    public static void main(String[] args) {
        //新建一个分支线程对象
        MyThread myThread=new MyThread();
        //启动线程
        //start方法的作用是:启动一个分支线程,在JVW中开辟一个新的栈空间
        //只要栈空间开辟出来,start方法就结束了,线程就启动成功了,启动成功的线程会自动调用run方法
        //run方法在分支线程的栈底部,main方法在主线程的栈底部,run和main是平级的
        myThread.start();
        //下面的代码运行在主线程中
        for (int i=0; i<10; i++) {
            System.out.println("主线程---> " + i);
        }
    }
}

1.2 实现Runnable接口,重写run方法

/**
 * 实现线程的第二种方式: 实现java.lang.Runnable接口,实现run方法
 */
class MyRunnable implements Runnable {

    @Override
    public void run() {
        //分支线程
        for (int i=0;i<10;i++) {
            System.out.println("分支线程---> " + i);
        }
    }
}

public class ThreadTest03 {

    public static void main(String[] args) {
        //将一个可运行对象封装成一个线程对象
        Thread thread=new Thread(new MyRunnable());
        //启动线程
        thread.start();
        //主线程
        for (int i=0; i<10; i++) {
            System.out.println("主线程---> " + i);
        }
    }
}

1.3 实现Callable接口,重写call方法,其中采用线程池

package com.szh.thread;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;

/**
 * 实现线程的第三种方式:实现Callable接口
 */
class MyCallable implements Callable<Object> {

    private String taskNum;

    public MyCallable(String taskNum) {
        this.taskNum=taskNum;
    }

    @Override
    public Object call() throws Exception {
        System.out.println(">>> " + taskNum + "任务启动");
        Date dateTest1=new Date();
        Thread.sleep(1000);
        Date dateTest2=new Date();
        long time=dateTest2.getTime() - dateTest1.getTime();
        System.out.println(">>> " + taskNum + "任务终止");
        return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
    }
}

public class ThreadTest16 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("-----程序开始运行-----");

        Date date1=new Date();

        int taskSize=5;
        //创建一个线程池
        ExecutorService pool= Executors.newFixedThreadPool(taskSize);
        //创建具有多个返回值的任务
        List<Future> list=new ArrayList<>();
        for (int i=0;i<taskSize;i++) {
            Callable callable=new MyCallable(i + " ");
            //执行任务并获取Future对象
            Future future=pool.submit(callable);
            //添加到list集合中
            list.add(future);
        }
        //关闭线程池
        pool.shutdown();

        //获取所有并发任务的运行结果
        for (Future f : list) {
            //从Future对象上获取到任务的返回值,打印输出到控制台
            System.out.println(">>> " + f.get().toString());
        }

        Date date2=new Date();
        System.out.println("-----程序结束运行-----,程序运行时间【" + (date2.getTime()-date1.getTime()) + "毫秒】");
    }
}

2.说说Runnable和Callable的区别?

  • Callable能够抛出checked exception(受检异常),而Runnable不可以。 
  • Callable可以返回一个泛型V,而Runnable不可以。
  • Callable可以获取线程的执行结果,Runnabl不可以。

3.说说wait和sleep方法的不同?

最大的不同是在等待时wait会释放锁,而sleep一直持有锁。wait通常被用于线程间交互,sleep通常被用于暂停执行。 

4.synchronized和volatile关键字的作用、区别?

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

● 保证了不同线程对这个变量进行操作时的可见性,不保证原子性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

● 禁止进行指令重排序。

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法级别的。
  • volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

5.什么是线程池,如何使用?

线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

在JDK的java.util.concurrent.Executors中提供了生成多种线程池的静态方法。 

5.1 创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行

ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

5.2 创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。

ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(10);

5.3 创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

5.4 创建一个固定大小的线程池,支持定时及周期性任务执行。

ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(10);

6.线程池的启动策略是什么?

1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

2、当调用execute()方法添加一个任务时,线程池会做如下判断:

(1)如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

(2)如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

(3)如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建线程运行这个任务;

(4)如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。

(5)当一个线程完成任务时,它会从队列中取下一个任务来执行。

(6)当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。

所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

7.说说synchronized和Lock的区别?

  • synchronized是关键字,Lock是类。
  • synchronized无法获取锁的状态,Lock可以。
  • synchronized会自动释放锁,Lock需要手动。
  • synchronized没有Lock锁灵活(Lock锁可以自己定制)。

8.请写一段简单的死锁代码

class MyThread1 extends Thread {
    Object o1;
    Object o2;

    public MyThread1(Object o1,Object o2) {
        this.o1=o1;
        this.o2=o2;
    }

    @Override
    public void run() {
        synchronized (o1) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2) {

            }
        }
    }
}

class MyThread2 extends Thread {
    Object o1;
    Object o2;

    public MyThread2(Object o1,Object o2) {
        this.o1=o1;
        this.o2=o2;
    }

    @Override
    public void run() {
        synchronized (o2) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1) {

            }
        }
    }
}

public class DeadLock {

    public static void main(String[] args) {
        Object o1=new Object();
        Object o2=new Object();

        //这两个线程共享o1、o2对象
        Thread thread1=new MyThread1(o1,o2);
        Thread thread2=new MyThread2(o1,o2);

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

9.请说出同步线程及线程调度相关的方法?

wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;

sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;

notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;

notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

注意:java 5通过Lock接口提供了显示的锁机制,Lock接口中定义了加锁(lock()方法)和解锁(unLock()方法),增强了多线程编程的灵活性及对线程的协调。

10.非阻塞算法CAS

首先我们需要了解悲观锁和乐观锁

悲观锁:假定并发环境是悲观的,如果发生并发冲突,就会破坏一致性,所以要通过独占锁彻底禁止冲突发生。有一个经典比喻,“如果你不锁门,那么捣蛋鬼就回闯入并搞得一团糟”,所以“你只能一次打开门放进一个人,才能时刻盯紧他”。

乐观锁:假定并发环境是乐观的,即虽然会有并发冲突,但冲突可发现且不会造成损害,所以,可以不加任何保护,等发现并发冲突后再决定放弃操作还是重试。可类比的比喻为,“如果你不锁门,那么虽然捣蛋鬼会闯入,但他们一旦打算破坏你就能知道”,所以“你大可以放进所有人,等发现他们想破坏的时候再做决定”。通常认为乐观锁的性能比悲观所更高,特别是在某些复杂的场景。这主要由于悲观锁在加锁的同时,也会把某些不会造成破坏的操作保护起来;而乐观锁的竞争则只发生在最小的并发冲突处,如果用悲观锁来理解,就是“锁的粒度最小”。但乐观锁的设计往往比较复杂,因此,复杂场景下还是多用悲观锁。首先保证正确性,有必要的话,再去追求性能。

乐观锁的实现往往需要硬件的支持,多数处理器都都实现了一个CAS指令,实现“Compare And Swap”的语义(这里的swap是“换入”,也就是set),构成了基本的乐观锁。CAS包含3个操作数:

需要读写的内存位置V

进行比较的值A

拟写入的新值B

当且仅当位置V的值等于A时,CAS才会通过原子方式用新值B来更新位置V的值;否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。一个有意思的事实是,“使用CAS控制并发”与“使用乐观锁”并不等价。CAS只是一种手段,既可以实现乐观锁,也可以实现悲观锁。乐观、悲观只是一种并发控制的策略。

以上是关于关于Java中多线程的主要内容,如果未能解决你的问题,请参考以下文章

Java——15个关于Java中多线程并发的面试题

iOS开发中多线程间关于锁的使用

java中多线程的线程同步死锁问题

Java基础__Java中多线程那些事

Java中多线程等待通知示例中的线程连接(1)用法

java中多线程实现方式