Java多线程系列之:多线程一些基本概念

一,基础概念

1,CPU核心数和线程的关系

  CPU核心数:最早的cpu是单核的。后来出现了多核cpu(2核,4核)

  CPU和线程的个数是1:1的关系。比如4核可以允许4个线程同时运行。后来intel提出了超线程的概念。使cpu和线程个数1:2。

2,CPU时间片轮转机制

  给每一个进程分配一个时间段,这个时间段就被称为进程的时间片 ---> 这个进程允许运行的时间。
  不同进程在cpu上执行,cpu需要进行不同进程之间的切换。每次切换需要耗费5000-20000个时钟周期。这其实是浪费了cpu的资源。
  我们在开发时,尽量减少让cpu去进行进程间的切换。

3,什么是线程和进程

  进程:程序运行进行资源分配的最小单位。一个进程内部有多个线程,多个会共享这个进程的资源
  线程:CPU调度的最小单位,线程不拥有资源。线程依附于进程。

4,并行和并发

  并行:某一个时间点,可以处理的事情(同一时刻,可以处理事情的能力)
  并发:与时间单位相关,某个时间段内,可以处理的事情(单位时间内可以处理事情的能力)

 

二,认识Java里的线程

1,Java里的程序天生就是多线程的,比如我们执行main方法时,并不是只有main这个主线程,还有别的线程在运行:

/**
 * java天生就是多线程的
 */
public class OnlyMain {
    public static void main(String[] args) {
        
        //Java虚拟机线程管理的接口。
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        
        //通过这个类可以拿到当前应用程序有多少个线程
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false,false);
        
        for (ThreadInfo threadInfo:threadInfos){
            System.out.println("["+threadInfo.getThreadId()+"] "+threadInfo.getThreadName());
        }
        /**
         * 打印结果:说明执行main方法时至少启动了5个线程
         [5] Monitor Ctrl-Break
         [4] Signal Dispatcher
         [3] Finalizer
         [2] Reference Handler
         [1] main                      main方法线程
         */
    }
}

2,启动新线程的三种方式:

/**
 * 创建线程的三种方式
 */
public class NewThread {

    /**
        方式一:扩展自Thread类
    */
    private static class UseThread extends Thread{
        @Override
        public void run() {
            System.out.println("i am extends Thread");
        }
    }

    /**
        方式二:实现Runnable
     */
    private static class UseRun implements Runnable{
        @Override
        public void run() {
            System.out.println("i am implements Runnable");
        }
    }

    /*
        方式三:实现Callable接口,允许有返回值
     */

    private static class UseCall implements Callable<String>{
        @Override
        public String call() throws Exception {
            System.out.println("i am implements Callable");
            return "CallResult";
        }
    }

    public static void main(String[] args) throws InterruptedException,ExecutionException{

        //Thread启动线程
        UseThread useThread = new UseThread();
        useThread.start();

        //Runnable启动线程
        UseRun useRun = new UseRun();
        new Thread(useRun).start();

        //Callable启动线程
        UseCall useCall = new UseCall();
        /**
         * 注意:
         * 1,Callable是不能直接交给Thread的
         * 2,可以把Callable包装成Runnable。FutureTask实现了Runnable接口
         * 3,包装成Runnable后,交给Thread
         */
        FutureTask<String> futureTask = new FutureTask<String>(useCall);
        new Thread(futureTask).start();
        //我们可以从Callable拿到返回值。注意:get()方法是阻塞的
        String result = futureTask.get();
        System.out.println(result);
    }
}

3,Java提供了Thread类,为什么还要提供Runnable接口?

从面向对象的角度思考:Java是单继承的,提供Runnable接口可以多实现。

4,如何让Java里的线程安全的停止工作?

4.1,三种方式:

  方式一:线程正常运行结束

  方式二:运行过程中抛出了异常

  方式三:Java提供的方法:

      suspend():调用该方法后,线程是不会释放资源的。比如该线程有锁,他不会释放这把锁。容易发生死锁
      stop():调用该方法后,强行终止线程。无法保证线程资源正常释放。
      resume():

  方式四:建议使用的方法(中断线程安全的方法):

       interrupt():中断一个线程,并不是强行关闭这个线程。只是把一个中断标志设置为true
      isInterrupted():判定当前线程是否处于中断状态。判断中断标志是否为true
      静态的interrupted():判定当前线程是否处于中断状态。 把中断标志改为false

4.2,注意:

java线程是协作式进行工作的,所以别的线程调用interrupt()并不是强行关闭这个线程,而是对该线程打个招呼。该线程会不会立即停止工作,完全由该线程自己做主。

示例代码:

public static void main(String[] args){
       UseRun useRun = new UseRun();
       Thread thread = new Thread(useRun);
       //main线程对thread线程打了个招呼,至于thread线程会不会停止,完全自己做主
       thread.interrupt();
}

 

三,深入理解Java线程

1,线程的生命周期

2,start()和run()的区别

  thread.start():开启一个新的线程,然后该线程会运行run()方法

  thread.run():就是单纯的调用run()方法,不会开启一个新的线程

3,了解yield():

  将线程从运行转到可运行状态(就绪态),放弃了当前cpu资源,进入就绪态,然后和其他线程一起去竞争cpu资源

4,线程的优先级

  设置线程的优先级:thread.setPriority(int priority); 优先级:1-10
  注意:理论上来说,线程的优先级越高,就越先被执行。有些操作系统会忽略我们设置的优先级。所以了解就行

5,守护线程

  和主线程共死的(主线程退出,守护线程也会退出。比如垃圾回收线程)

public class DaemonThread {
    private static class UseThread extends Thread{
        @Override
        public void run() {
            while (true){
                System.out.println(Thread.currentThread().getName()+"i am daemon Thread");
            }
        }
    }
    public static void main(String[] args)throws InterruptedException {
        Thread endThread = new UseThread();
        /**
         * 注意:必须在start()方法之前,
         *       如果主线程执行完毕,守护线程也停止.
         */
        endThread.setDaemon(true);
        endThread.start();
        Thread.sleep(1);
        /**
         * 在main主线程睡眠的这一1ms期间,守护线程一直在运行,当main线程执行完毕,守护线程也退出
         * 打印结果:
         *  Thread-0i am daemon Thread
            Thread-0i am daemon Thread
            Thread-0i am daemon Thread
            ...
         */
    }
}

 

四,线程间的共享

1,什么是线程间的共享

2,如何实现线程间的共享

2.1,synchronized内置锁

分为类锁和对象锁:

  对象锁:锁代码块,锁方法
  类锁:static+synchronized : public static synchronized void method(){}

能不能同时运行:

  同一个对象锁,不能同时运行
  两个不同的对象锁,可以同时运行
  一个类锁和一个对象锁,可以同时运行
  两个类锁,不能同时运行。 因为每个类的Class对象只有一个,所以还是同一把锁。

区别:

  对象锁,锁的是new出来的对象实例
  类锁,锁的是每个类的Class对象

2.2,volatile关键字:最轻量的同步机制

  保证了可见性:修改了数据后,强制把数据刷到主内存。读取数据时,强制从主内存中读取数据。
  但是不保证原子性:即 a = a+1;不是一步完成的。操作系统会执行好几条指令才完成加1操作。

Volatile的使用场景:
只有一个线程写,多个线程读。

2.3,ThreadLocal的使用

每个线程只是用自己线程的变量,把数据和当前线程绑定

案例:我在当前线程中保存该用户的信息,那么在用户的请求线程进入程序时把用户的信息和当前线程绑定,当用户的请求线程结束,我再把这个线程移除。

如何实现:

public class RequestHolder {

    private static final ThreadLocal<String> userHolder = new ThreadLocal<>();
public static void add(String sysUser) {
        userHolder.set(sysUser);
    }public static String getCurrentUser() {
        return userHolder.get();
    }public static void remove() {
        userHolder.remove();
    }
}

在拦截器的preHandle()方法中把线程和用户信息绑定,在postHandle()方法中把该线程销毁

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        RequestHolder.add(authorization);//这里我绑定的是token
        return true;
    }
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 正常返回时, 显式回收threadLocal里的信息
        RequestHolder.remove();

    }

在我需要获取用户信息的地方(比如Service中的方法),通过线程对应的用户信息

String token  = RequestHolder.getCurrentUser();

 

五,线程间的协作

1,轮询:难以保证及时性,资源开销很大

2,等待和通知

等待和通知的标准范式:

  等待方:
    要去获取对象的锁,
    然后在循环里判断条件是否满足,不满足调用wait方法。
    条件满足,执行业务逻辑
  通知方:
    获取对象的锁
    改变条件
    通知所有等待在对象的线程

3,方法:

  wait():等待着获取对象的锁
  wait(1000):等待超时,超过一定时间就不等待了。
  notify:通知一个线程
  notifyAll:通知所有等待同一把锁的线程

4,join()方法
面试问题:有线程A和线程B,如何保证线程B一定在线程A执行完以后才执行?
  方法一:join()
  方法二:countDownLatch
解释:如果线程A执行了线程B的join方法,线程A必须等待线程B执行完了以后,线程A才能继续自己的工作。

5,yield(),sleep(),wait(),notify()等方法对锁的影响
  线程在执行yield()以后,持有的锁是不释放的
  sleep()方法调用以后,持有的锁是不释放的
  wait():在调用wait()方法之前,必须要持有锁。在调用wait()方法以后。锁就会被释放(虚拟机进行释放),当wait方法返回时,线程会重新持有锁
  notify():在调用之前,必须要持有锁。调用notify()方法本身是不会释放锁的,只有synchronized代码块执玩才释放锁
  notifyAll():同notify()
比如:public synchronized void changeKm(){
    this.km = 101;
    notify();//当执行完这行代码时,此时还没有释放锁。
    System.out.println("处理业务逻辑"); //执行完这一行代码后,才释放锁。
   }

 

posted @ 2019-06-27 13:58  inspire0x001  阅读(331)  评论(0编辑  收藏  举报