一、线程

1.什么是线程,进程

进程:进程是操作系统分配资源的基本单位

线程:线程是进程中的一个执行单元(一个独立执行的任务),是cpu执行的最小单元

2.java中如何创建线程

1.线程的四种创建方式

在 Java 中,创建线程主要有四种方式,适用于不同场景:

继承 Thread 类:重写run方法,直接创建子类对象并调用start启动。

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行");
    }
}
// 使用:new MyThread().start();

实现 Runnable 接口:重写run方法,通过new Thread(任务对象)创建线程。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程执行");
    }
}
// 使用:new Thread(new MyRunnable()).start();

实现 Callable 接口:与 Runnable 相比,Callable 可以返回结果并抛出异常,配合 Future 获取结果。

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 100;
    }
}
// 使用:Future<Integer> future = new ThreadPoolExecutor().submit(new MyCallable());

线程池:通过ThreadPoolExecutor创建线程池,复用线程资源(后文详细讲解)。

2.线程中常用的方法

run() ,call()

Thread类中的方法:

                start():启动线程的,把线程注册到操作系统 private native void start0();

                sleep():让线程休眠指定的时间

                join():让其他线程等待当前线程执行完成后再执行.

                yeild():线程礼让

                setDaemon():设置线程为守护线程

                currentThread(): 获得当前正在执行的线程

                wait():让线程等待,只能被其他线程唤醒

                notify(),notifyAll():唤醒被wait等待的线程


3.线程的状态

3.多线程

1.什么是多线程

在程序中(进程)可以创建多个线程来分别执行不同的任务

2.多线程的优缺点

优点: 可以提高程序执行效率

缺点:  线程多了,需要操作系统进行管理的,占用开销的(不是啥事情都创建线程执行的.)

​          多个线程同时访问共享资源(数据)会出现问题

3.线程同步

​    加锁排队  (饭堂买饭 排队+加锁  一次只能有一个买饭)

​    synchronized关键字

​    synchronized修饰方法   非静态方法锁对象是this   静态方法是类的Class对象

​    synchronized修饰代码块  

ReentrantLock

public class ReentrantLockDemo  implements  Runnable{

    int num = 10;
    /*
           ReentrantLock是java.util.concurrent.locks包下的类,
           是java代码实现的一种锁控制
           只能手动的加锁和手动的释放锁
           只能对某段代码块加锁,不能给整个方法加法
     */
    ReentrantLock reentrantLock = new ReentrantLock();

    @Override
    public void run() {

        while (true){
          try { 
              reentrantLock.lock();//加锁   
              if (num > 0) {
                  System.out.println(Thread.currentThread().getName() + "买到第" + num + "张票");
                  num--;
              } else {
                  break;
              }
          }finally {
              reentrantLock.unlock();//释放锁
          }

        }
    }
}

4.死锁

多个线程相互持有对方需要的锁对象不释放,二形成的一种相互等待获取锁的现象,

死锁发生后,程序不会报错,只能相互等待,不继续向后执行了.

如何避免死锁发生:

避免锁的嵌套使用

避免多个同步代码块中的锁相互使用

口语描述死锁:

线上1和线程2  同时访问两个代码块, 线程1访问的同步代码块使用A锁,在A锁的同步代码块又使用了B锁.

线程2在访问的同步代码块中使用B锁,在B锁的同步代码块中又用到了A锁,有可能形成死锁

5.线程通信(生产者,消费者模型)

在线程同步的基础上进行的,两个线程相互牵制执行

wait();  线程等待

notify(); 唤醒等待的线程

只能在同步代码块(同步方法)中使用, 还只能通过同步锁对象调用

案例:交替打印数字

public class PrintNum  implements  Runnable{

    int num = 0;
    Object obj = new Object();

    @Override
    public void run() {

        while (true){
          synchronized(obj){
              obj.notify();
              System.out.println(Thread.currentThread().getName()+":"+num++);
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  throw new RuntimeException(e);
              }
              try {
                  obj.wait();
              } catch (InterruptedException e) {
                  throw new RuntimeException(e);
              }
          }
        }
    }
}

wait()和sleep()区别:

所属的类不同:  wait()属于Object类    sleep()属于Thread类

阻塞后唤醒方式不同:  wait()让线程等待后,需要让另一个线程通过notify()唤醒,如果不唤醒一直等待

​                                   sleep()休眠执定的时间,时间到了后,自动唤醒

释放锁不同:  sleep()不会释放锁

​                         wait()会释放锁

二、并发编程

后面谈论的线程内容,几乎都是在多线程对同一共享数据操作的基础上进行.

1.多线程优缺点

优点: 可以提高程序执行效率

缺点: 线程多了,需要操作系统进行管理的,占用开销的(不是啥事情都创建线程执行的.)

​          多个线程同时访问共享资源(数据)会出现问题

并发(高并发): 计算机领域指的是同一时刻只能有一个任务执行,多个任务依次执行

​                          汉语并发指的是同时执行

并发执行: 在很多用户同时访问时,应当让多个用户并发执行(一个时间段内依次执行)

并行执行: 在同一个时间点上,多个任务同时执行

2.并发编程核心问题

​     多个线程同时对同一个资源访问的情况下.  为什么会出现问题.

2.1不可见性

​    首先了解java程序运行的内存模型(JMM)

​    java在线程操作时,先把主内存中的数据加载到线程的工作内存中,然后对数据进行操作,但是线程A在自己的工作内存中操作玩出,线程B不知道,做了同样的操作,最终结果和预期结果不一样

2.2乱序性(无序性)重排序

   系统为了优化,在执行某些指令时,会将一些看起来没关系的指令执行顺序改变,但是在,某些情况下会产生问题.

  1   int  a = 10;    

  2    int  b =  从数据库读取

  3    int  c = a+b;

2.3非原子性

  i++;在多线程情况下时线程安全的吗?

i++高级语言,在底层执行时,会被分为3条件`

  加载i

  i+1

 i=2

线程的切换执行会带来原子性问题

java内存模型为每个线程提供工作内存(缓存),导致不可见性问题

系统指令优化会把一些指令顺序重排序(在执行一些较为耗时的指令时,把一些其他指令先行执行),可能导致程序无法正常执行

线程切换执行会打破指令执行的原子性 ,例如++操作  分为三条指令, 但是在执行这3条指令时,cpu可能在执行时,切换到其他线程执行,打破原子性执行.

2.4如何解决以上3个问题:

        2.4.1 volatile

                volatile关键可以解决不可见性和乱序性

                volatile修饰的共享变量,在被一个线程操作后,可以做到立即对其他线程可见 (volatile底层实现   内存屏障)

                volatile修饰的变量禁止对其的执行顺序重排序,volatile只能解决不可见性和重排序问题,不能解决非原子性问题

        2.4.2 如何解决非原子性问题:

                1. 加锁(ReentrantLock和synchronized)

                2. 使用原子类来解决++在多线程中非原子性问题
                   AtomicInteger 是一个原子类,能够在不加锁的情况下,实现多线程++操作不出问题,结果正确

   private  static AtomicInteger atomicInteger = new AtomicInteger(0);
   atomicInteger.incrementAndGet();  自增并获得

        2.4.3 CAS 机制

                CAS(Compare-And-Swap) :比较并交换

                        特点:   不加锁实现对变量++操作保证原子性.

                        优点:  线程不会进入到阻塞状态,一直自旋,效率高

                        缺点:  线程一直自旋,对cpu开销大,  所以原子类适用于线程少的情况

3.Java 中的锁分类

3.1乐观锁/悲观锁

乐观锁:  就是没有加锁的实现. AtomicInteger中的实现就是不加锁的,通过自旋比较实现(CAS)

悲观锁: 就是加锁的实现,认为不加锁是会出问题的  ,ReentrantLock和synchronized都是悲观锁

3.2可重入锁

ReentrantLock和synchronized都是可重入锁

可重入锁又名递归锁, 指的是一个线程在外层方法获得锁时,可以直接进入到内层的加锁的方法中.

3.3自旋锁

​    指的是对synchronized获得锁的一种描述(特点), 线程在获得锁时,是自旋的不断尝试去获得锁

3.4公平锁/非公平锁

公平锁:  就是排队获得锁,有先来后到  ReentrantLock 既可以是公平锁也可以是非公平锁

非公平锁: 就是抢锁,谁抢到谁执行, 有可能后来的线程先抢到锁  synchronized ReentrantLock

3.5读写锁

ReentrantReadWriteLock

特点: 读读不互斥,  读写互斥, 写写互斥

适合读(查询)多,写少的场景, 提高读的效率

3.6共享锁和独占锁

独占锁:  synchronized ReentrantLock都是独占锁,就是有我没他,一次只能有一个线程执行.

共享锁:  一个锁可以被多个线程持有,  读写锁中的读锁就是共享锁
 

4.synchronized锁的实现

jdk1.7之后,对synchronized锁进行了优化(jdk7之前synchronized锁没有状态,都是自旋的获取锁),jdk7之后为synchronized锁设计了不同的状态.

无锁状态:   没有线程进入到同步代码块就是无锁状态

偏向锁状态:  只有一个线程访问同步代码块时,同步锁中记录线程id,下次线程访问时,可以快速的获得锁.

轻量级锁状态: 当线程数量大于1个之后,锁状态由偏向锁升级为轻量级锁, 线程不会阻塞,以自旋方式获得锁,提高获取锁的效率.

重量级锁状态: 当锁状态为轻量锁时,如果线程自旋到一定次数还获取不到锁,那么锁会升级为重量级锁,让获取不到锁的线程进入到阻塞状态,等待操作系统调度.

使用synchronized锁的时候,必须为锁提供一个同步锁对象的,此对象就是用来记录锁状态的

对象中有一个区域叫对象头,对象头中有一块区域叫mark word,记录对象运行时的一些数据,如锁状态,哈希值,GC分代年龄,当前线程id.
 

synchronized是java中内置的一种的锁,底层实现是靠底层指令进行控制的,

使用时必须提供一个同步锁对象,用来记录锁的各种状态.

5.AQS

AbstractQueuedSynchronizer 抽象同步队列, 并发包下面很多类的底层实现都会用到

内部有一个int类的变量state,用来记录有没有线程使用

内部会构建一个队列,用来存储没有获得锁的线程

6.ReentrantLock锁实现

ReentrantLock 基于 AQS的,

ReentrantLock 可以实现公平锁和非公平锁

内部结构

公平和非公平的区别

7.线程池

7.1理解池的概念

        字符串常量池    String s1 = "abc";   String  s2="abc";   s1==s2;

        数据库连接池 Connection  每次链接创建,每次用完销毁  创建对象是要花费时间的

        Integer  -128---+127也有缓存池,当定义一个Integer类型的变量时,如果数值大小在-128---+127直接,那么该变量则会直接在缓存池中引用,否则才会new出新的对象。

int a=17;int b=17;那么a==b为true;

int a=170;int b=170;那么a==b为false;

7.2 线程池

package com.threadPool;

import java.util.concurrent.*;

public class Test {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 200, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardPolicy());

        for(int i=1;i<=8;i++){
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);//添加任务到线程池

        }
        executor.shutdown();
    }
}

package com.threadPool;

public class MyTask implements Runnable {

    private int taskNum;

    public MyTask(int num) {
        this.taskNum = num;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task "+taskNum+"执行完毕"+":"+Thread.currentThread().getName());
    }
}

如果每次执行任务时,都去创建线程对象,用完销毁,频繁的创建也是比较占资源的.

jdk5之后,提供ThreadPoolExecutor类来实现线程池.

好处: 避免了频繁的创建也是比较占资源的, 统一管理线程

ThreadPoolExecutor构造方法中的7个参数:

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 

        corePoolSize:  核心线程池数量

        maximumPoolSize: 最大线程池数量

        keepAliveTime:  非核心线程池中的线程, 空闲多久后销毁

        unit: 为keepAliveTime指定时间单位

        workQueue: 等待的线程队列额

        threadFactory: 创建线程的工厂

        RejectedExecutionHandler: 拒绝策略

7.2.1线程池的执行流程

7.2.2线程池中的队列

        线程池有以下工作队列:

                ArrayBlockingQueue

                LinkedBlockingQueue

7.2.3线程池的拒绝策略

        1.AbortPolicy    直接抛出异常

        2.CallerRunsPolicy    拒绝后,由提交任务的线程执行此任务(如main线程)

        3.DiscardOldestPolicy 丢弃队列中等待时间最长的那一个

        4.DiscardPolicy  丢弃最后来的无法执行的任务

7.2.4向线程池提交任务的两种方法

        execute 与 submit 的区别

                void  execute 适用于不需要关注返回值的场景

                submit 方法适用于需要关注返回值的场景。

7.2.5关闭线程池

        shutdownNow    立刻关闭,即使还有未执行完的任务

        shutdown  等待所有任务执行完了再关闭

8.ThreadLocal

作用:  为每个线程提供一个变量副本

使用:

  //创建ThreadLocal对象,为每个线程自动的提供一个变量副本
    static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
         @Override
         protected Integer initialValue() {
             return 1; //初始化变量
         }
     };
    public static void main(String[] args) {
        /*
             这种方式在两个线程中对num进行操作,这个num是同一个
         */
        //线程1
          new Thread(new Runnable() {
              @Override
              public void run() {
                   threadLocal.set(10);
                   threadLocal.set(threadLocal.get()+5);
                  System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
              }
          }).start();
       //线程2
          new Thread(new Runnable() {
              @Override
              public void run() {
                  threadLocal.set(20);
                  threadLocal.set(threadLocal.get()+10);
                  System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
              }
          }).start();
    }

内部实现

ThreadLocal 内存泄漏问题

        内存溢出: 内存不够用了

        内存泄漏: 一些对象已经不再使用,但是虚拟机又不能回收的对象( 例如: 数据库连接对象,IO流对象,Socket 提供close)

        如果使用ThreadLocal不当,会造成内存泄漏问题。

        对象与引用关系

        Object obj = new  Object();   强引用

        obj= null;          没有引用

软引用: 被SoftReference对象管理的引用, 内存充足时,不回收该对象,一旦内存不足时,

就会回收软引用管理的对象.

 Object o1 = new Object();
 SoftReference<Object> softReference = new SoftReference<Object>(o1);

弱引用: 被WeakReference对象管理的引用,  只要进行垃圾回收,就会被回收掉

 Object o1 = new Object();
 WeakReference<Object> weakReference = new WeakReference<Object>(o1);

ThreadLocal被弱引用管理的, 下次垃圾回收到来时,ThreadLocal会被回收掉, 造成ThreadLocalMap中的不存在了,

但是value还被外界引用, 所以ThreadLocalMap就不能被回收, 造成了内存泄漏.

所以,  正确的使用ThreadLocal方式是在用完之后, 主动删除ThreadLocalMap中的数据.

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐