多线程学习笔记

线程和进程的区别

1、进程是资源分配的最小单位,线程是CPU调度和执行的单位。
2、进程拥有独立的堆栈空间和数据段,因此安全性高,但是系统开销比较大,且进程之间的通信相对复杂;线程拥有独立的堆栈空间,但是共享数据段,因此系统开销小通信比较方便。
3、一个进程至少有一个线程,可以包含多个线程。

创建线程有哪几种方式

1. 继承Thread类:让一个类继承Thread重写run方法,然后把它new出来,这便是创建了一个新线程。
2. 实现Runnable接口:通过实现Runnable接口的run方法,可以得到一个“可被执行的任务”,然后在new Thread的时候将这个任务传进去。
3. Callable+FutureTask
首先让一个类实现Callable(泛型)接口的call方法,这一步是写一个“可被调用的任务”;
再new一个FutureTask(”未来的任务“),同时将上一步的Callable传进去;
最后new一个Thread,同时将Future传进去,就可以得到一个带返回值的线程了。

线程状态

线程的五种状态:新生、就绪、运行、阻塞和死亡。
新生状态:线程对象一旦创建就进入了新生状态。
就绪状态:线程对象调用start方法后,该线程就进入了就绪状态,等待CPU调度执行。
运行状态:线程抢到CPU资源就进入了运行状态,开始运行run函数当中的代码。
阻塞状态:线程运行状态时被暂停进入阻塞状态。sleep、wait或同步锁定都可以导致线程阻塞。
死亡状态:如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪。

JDK1.5 Thread.State 源码中定义线程的状态有6个
public enum State {
//新生
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待
WAITING,
//超时等待
TIMED_WAITING,
//终止
TERMINATED;
}

wait 和 sleep 方法的区别

sleep是Thread类中的一个方法,使用时必须捕获异常,目的是将一个线程睡眠一段时间,睡眠期间一直持有锁,可以在任意地方使用。
wait是Object类中的一个方法,使用时不需要捕获异常,目的是将一个线程挂起直到超时或者该线程被唤醒,wait会释放锁,只能在同步控制方法或者同步控制块里面使用,一般用于线程间的交互。

notify()和notifyAll()的区别

当一个线程进入wait之后,就必须等其他线程notify/notifyall,使用notifyall,可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。如果没把握,建议notifyAll,防止notigy因为信号丢失而造成程序异常。

wait, notify 和 notifyAll这些方法为什么不在thread类里面?

JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

线程同步和线程异步

线程同步:是多个线程同时访问同一资源时,一个线程拿到资源后,其他线程必须等待该线程访问结束后才能获取资源。特点是数据安全,效率不高,单线程不需要同步。
线程异步:多个线程访问同一资源时,该资源正在被某个线程使用,但是其他线程仍然可以访问使用到。特点是效率高,数据不安全。

线程同步的实现方式

1. 同步方法:给一个方法增加synchronized关键字后就可以使它成为同步方法,这个方法可以是静态方法和非静态方法,但是不能是抽象类的抽象方法,也不能是接口中的接口方法。

2. 同步代码块:给一部分代码增加synchronized关键字后就可以使它成为同步代码块。同步代码块是通过锁定一个指定的对象,来对同步块中包含的代码进行同步;而同步方法是对这个方法块里的代码进行同步,而这种情况下锁定的对象就是同步方法所属的主体对象自身。

public class MybanRunnable implements Runnable{

    private Bank bank;

    public MybanRunnable(Bank bank) {
        this.bank = bank;
    }
    @Override
    public void run() {
        for(int i=0;i<10;i++) {
            bank.save1(100);
            System.out.println("账户余额是---"+bank.getAccount());
        }

    }
}

class Bank{
    private int account = 100;

    public int getAccount() {
        return account;
    }
    //java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。
    // 在调用该方法前,需要获得内置锁,否则就处于阻塞状态
    //同步方法
    public synchronized void save(int money) {
        account+=money;
    }
    public void save1(int money) {
        //同步代码块
        synchronized(this) {
            account+=money;
        }

    }

    public void userThread() {
        Bank bank = new Bank();
        MybanRunnable my1 = new MybanRunnable(bank);
        System.out.println("线程1");
        Thread th1 = new Thread(my1);
        th1.start();
        System.out.println("线程2");
        Thread th2 = new Thread(my1);
        th2.start();

    }
}

3. 使用特殊域变量(volatile)实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制,相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

//只给出要修改的代码,其余代码与上同
//需要同步的变量加上volatile
private volatile int account = 100;

public int getAccount() {
    return account;
}
//这里不再需要synchronized
public void save(int money) {
    account += money;
}

4. 使用重入锁实现线程同步:在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法具有相同的基本行为和语义,并且扩展了其能力。

private int account = 100;
private Lock lock = new ReentrantLock();
public int getAccount() {
    return account;
}
public  void save(int money) {
    //加锁
    lock.lock();
    try {
        account+=money;
    } finally {
        //释放锁
        lock.unlock();
    }
}

5. 使用局部变量实现线程同步:如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

//使用ThreadLocal类管理共享变量account
private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){
    @Override
    protected Integer initialValue(){
        return 100;
    }
};
public void save(int money){
    account.set(account.get()+money);
}
public int getAccount(){
    return account.get();
}

6. 使用阻塞队列实现线程同步

public class BlockingSynchronizedThread {
    /**
     * 定义一个阻塞队列用来存储生产出来的商品
     */
    private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
    /**
     * 定义生产商品个数
     */
    private static final int size = 10;
    /**
     * 定义启动线程的标志,为0时,启动生产商品的线程;为1时,启动消费商品的线程
     */
    private int flag = 0;

    private class LinkBlockThread implements Runnable {
        @Override
        public void run() {
            int new_flag = flag++;
            System.out.println("启动线程 " + new_flag);
            if (new_flag == 0) {
                for (int i = 0; i < size; i++) {
                    int b = new Random().nextInt(255);
                    System.out.println("生产商品:" + b + "号");
                    try {
                        queue.put(b);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("仓库中还有商品:" + queue.size() + "个");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } else {
                for (int i = 0; i < size / 2; i++) {
                    try {
                        int n = queue.take();
                        System.out.println("消费者买去了" + n + "号商品");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("仓库中还有商品:" + queue.size() + "个");
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        BlockingSynchronizedThread bst = new BlockingSynchronizedThread();
        LinkBlockThread lbt = bst.new LinkBlockThread();
        Thread thread1 = new Thread(lbt);
        Thread thread2 = new Thread(lbt);
        thread1.start();
        thread2.start();
    }
}

7. 使用原子变量实现线程同步:原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作即-这几种行为要么同时完成,要么都不完成。在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

private AtomicInteger account = new AtomicInteger(100);
public AtomicInteger getAccount() {
    return account;
}
public void save(int money) {
    account.addAndGet(money);
}

死锁

死锁就是指两个或两个以上的线程在执行程序的过程中,竞争共享资源时都在等待对方释放资源而停止执行程序的现象称为死锁,若无外力,他们都无法继续执行下去。

死锁产生条件

互斥条件:一个资源只能被一个线程占有,当这个资源被占有后其他线程就只能等待。
不可剥夺条件:当一个线程不主动释放资源时,此资源一直被拥有线程占有。
请求并持有条件:线程已经拥有了一个资源后,又尝试请求新的资源。
循环等待条件:产生死锁一定是发生了线程资源循环等待链。

JAVA中的锁类型

  • 悲观锁和乐观锁
    悲观锁:当前线程去操作数据的时候,总是认为别的线程会去修改数据,所以每次操作数据的时候都会上锁,别的线程去操作数据的时候就会阻塞,比如synchronized;
    乐观锁:当前线程每次去操作数据的时候都认为别人不会修改,更新的时候会判断别人是否会去更新数据,通过版本来判断,如果数据被修改了就拒绝更新,例如cas是乐观锁,但是严格来说并不是锁,通过原子性来保证数据的同步,例如数据库的乐观锁,通过版本控制来实现,cas不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响。
    总结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会比悲观锁高
  • 公平锁和非公平锁
    公平锁:有多个线程按照申请锁的顺序来获取锁,就是说,如果一个线程组里面,能够保证每个线程都能拿到锁,例如:ReentrantLock(使用的同步队列FIFO)
    非公平锁:获取锁的方式是随机的,保证不了每个线程都能拿到锁,会存在有的线程饿死,一直拿不到锁,例如:synchronized,ReentrantLock
    总结:非公平锁性能高于公平锁,更能重复利用CPU的时间
  • 可重入锁和不可重入锁
    可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不会产生死锁
    不可重入锁:在当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
    总结:可重入锁能一定程度的避免死锁,例如:synchronized,ReentrantLock
  • 自旋锁和非自旋锁
    自旋锁:一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁。
    非自旋锁:没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。
    总结:自旋锁不会发生线程状态的切换,一直处于用户态,减少了线程上下文切换的消耗,缺点是循环会消耗CPU
  • 共享锁和独享锁
    共享锁:也叫读锁,用于资源数据共享,加锁后可以被其他线程持有并发读取数据,但不能修改、增加、删除数据。
    独享锁:也叫写锁、独占锁、独享锁,该锁每一次只能被一个线程所持有,加锁后任何线程试图再次加锁都会被阻塞,直到当前线程解锁。例如:线程A对data加上排它锁后,则其他线程不能再对data加任何类型的锁,获得互斥锁的线程既能读数据又能修改数据

Synchronized实现原理

  1. synchronized同步代码块:synchronized关键字经过编译之后,会在同步代码块前后分别形成monitorenter和monitorexit字节码指令,在执行monitorenter指令的时候,首先尝试获取对象的锁,如果这个锁没有被锁定或者当前线程已经拥有了那个对象的锁,锁的计数器就加1,在执行monitorexit指令时会将锁的计数器减1,当减为0的时候就释放锁。如果获取对象锁一直失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
  2. 同步方法:方法级的同步是隐式的,无须通过字节码指令来控制,JVM可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用的时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先持有monitor对象,然后才能执行方法,最后当方法执行完(无论是正常完成还是非正常完成)时释放monitor对象。在方法执行期间,执行线程持有了管程,其他线程都无法再次获取同一个管程。

lock实现原理

New一个实现类比如ReentrantLock,在多线程下访问(互斥)共享资源时,访问前加锁,访问结束以后解锁,解锁的操作推荐放入finally块中

//根据不同的实现Lock接口类的构造函数得到一个锁对象
Lock l = new ReentrantLock();
//获取锁位于try块的外面
l.lock();
try {
    //访问受此锁保护的资源代码块
} finally {
    l.unlock();
}

synchronized与Lock两者区别

  1. Synchronized 内置的关键字,Lock是一个Java接口。
  2. Synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁。
  3. Synchronized 会自动释放锁,Lock必须手动释放锁,如果不释放锁会造成死锁。
  4. Synchronized 线程1(获取锁,阻塞),线程2(一直等待线程1释放锁);Lock锁就不一定会一直等下去。
  5. Synchronized 可重入锁,不可以中断的,非公平;Lock:可重入锁,可以判断锁,非公平(可以自己设置)。
  6. Synchronized 适合少量代码的同步问题,Lock适合锁大量的同步代码。

安全集合类

CopyOnWriteArrayList

相当于线程安全的ArrayList,底层是一个被volatile修饰的数组,保证了数据被修改后,其他线程可见。

CopyOnWriteArrayList添加元素时,先获取独占锁,将添加功能加锁,获取原来的数组,并得到其长度,创建一个原来数组长度+1的数组,并拷贝原来的元素给新数组,追加元素到新数组末尾,指向新数组,释放锁。

这个过程是线程安全的,COW(写入时复制)的核心思想就是每次修改的时候拷贝一个新的资源去修改,add()方法再拷贝新资源的时候将数组容量+1,这样虽然每次添加元素都会浪费一定的空间,但是数组的长度正好是元素的长度,也在一定程度上节省了扩容的开销。
总结:Vector和CopyOnWriteArrayList都是线程安全的List,底层都是数组实现的,Vector的每个方法都进行了加锁,而CopyOnWriteArrayList的读操作是不加锁的,因此CopyOnWriteArrayList的读性能远高于Vector,Vector每次扩容的大小都是原来数组大小的2倍,而CopyOnWriteArrayList不需要扩容,通过COW思想就能使数组容量满足要求。两个集合都实现了RandomAccess接口,支持随机读取,因此更加推荐使用for循环进行遍历。在开发中,读操作会远远多于其他操作,因此使用CopyOnWriteArrayList集合效率更高。

CopyOnWriteArraySet

相当于线程安全的HashSet,底层是一个不可变的CopyOnWriteArrayList,并不是散列表。

ConcurrentHashMap

相当于线程安全的HashMap,要避免 HashMap 的线程安全问题,有多个解决方法,比如改用 HashTable 或者 Collections.synchronizedMap() 方法。但是这两者都有一个问题,就是性能,无论读还是写,他们两个都会给整个集合加锁,导致同一时间的其他操作阻塞。ConcurrentHashMap 的优势在于兼顾性能和线程安全,一个线程进行写操作时,它会锁住一小部分,其他部分的读写不受影响,其他线程访问没上锁的地方不会被阻塞。

线程池

在开发中经常频繁的创建和销毁线程对系统的性能影响很大,所以可以提前创建多个线程放入线程池中,使用时直接获取,用完后放回池中。避免了频繁创建和销毁线程,实现重复利用,提高系统响应速度,同时也便于线程的管理。

线程池的创建

JDK5.0起提供了线程池相关API,开发中常用的有ExecutorService 和 Executors。
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。
ExecutorService:继承于Executor接口,提供了更多更强大的方法,比如submit(Callable task):执行任务,有返回值,一般用来执行Callable,shutdown() :关闭连接池,它的常见子类 ThreadPoolExecutor

线程池的工作原理

Executors的四个方法

  1. newCacheTreadPool
//创建一个可以缓存的线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();


特点:如果有一个新的任务需要执行时,池内所有线程都繁忙,那么newCacheTreadPool就会添加新线程来处理该任务,newCacheTreadPool大小取决于操作系统能够创建的最大线程大小,如果部分线程空闲(60秒不执行任务)就会回收这些线程。
缺点:最大线程数量不设限上。可以无限创建线程,如果任务提交较多,就会造成大量的线程被启动,很有可能造成OOM异常,甚至导致CPU线程资源耗尽。
2. newFixedThreadPool

//创建一个固定数量的线程池。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(8);


特点:如果线程数没有达到固定数量,那么每次提交一个任务线程池内就会创建一个线程,直到线程数量达到线程池固定数量,线程池达到固定数量后就会保持数量稳定,如果其中一个线程出现异常,那么线程就会重新创建一个线程。如果执行一个任务时,池内所有线程都处于繁忙状态时,该任务就会进入阻塞队列中等待执行。
缺点:阻塞队列无界,队列很大,很有可能导致JVM出现OOM(Out Of Memory)异常,即内存资源耗尽
3. newScheduledThreadPool

/创建一个可调度线程池
ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);



特点:可以创建定长的、支持定时任务,周期任务执行
缺点:线程数量无上界,会导致创建大量的线程,从而导致OOM
4. newSingleThreadExecutor

//创建一个单线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();


特点:单线程的线程池,线程存活时间是无限的,可以保证任务按提交顺序逐个执行。
缺点:问题和固定数量线程池一样,阻塞队列无界
总结:建议直接使用线程池ThreadPoolExecutor的构造器创建线程池

ThreadPoolExecutor的七个参数

参数含义解释
corePoolSize核心线程数核心线程生命周期无限,即使空闲也不会死亡。
maximumPoolSize最大线程数corePoolSize<线程总数<maximumPoolSize :等任务队列满时才会创建线程。线程总数<corePoolSize<maximumPoolSize:即使有空闲线程,也会创建线程。
keepAliveTime空闲线程存活时间线程总数大于核心线程数时,超过keepAliveTime时间将会回收非核心线程
unit时间单位 (时/分/秒等)keepAliveTime的时间单位
workQueue任务阻塞队列存放任务(Runnable)的容器
threadFactory线程工厂为线程池提供创建新线程的线程工厂
handler拒绝策略新增一个任务到线程池,如果线程池任务队列超过最大值之后,并且已经开启到最大线程数时,可选择抛出异常

线程池的拒绝策略

  1. AbortPolicy:拒绝策略
    新任务就会被拒绝,并且抛出RejectedExecutionException异常。该策略是线程池默认的拒绝策略
  2. DiscardPolicy:抛弃策略
    新任务就会直接被丢掉,并且不会有任何异常抛出
  3. DiscardOldestPolicy:抛弃最老任务策略
    将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列(一般队头元素最老)
  4. CallerRunsPolicy:调用者执行策略
    新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务

线程池为什么需要使用(阻塞)队列

  1. 因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。
  2. 创建线程池的消耗较高。

原子操作

原子操作:一个或多个操作在CPU执行过程中不被中断的特性,要么全部完成,要么全部失败。Java中原子操作通过锁和自旋CAS实现。

Volatile

volatile是java虚拟机提供的轻量级的同步机制,主要有三个特效:

  1. 保证可见性
  2. 不保证原子性
  3. 由于内存屏障,禁止指令重排

CAS理解

cas是compareandswap的简称,从字面上理解就是比较并交换,简单来说:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。

Java无法直接操作内存,通过调用Unsafe类里面的方法来操作内存。Unsafe类:是一个被final修饰的最终类,里面的方法大多数都是被native修饰的方法,native修饰的方法可以调用C++来操作内存。

没有达到预期值就不停的循环(自旋锁)

CAS隐患

1. ABA问题
CAS存在一种场景,两个线程去拿同一个共享变量值,某一个线程拿到变量值原来是A,然后变成了B,最后又变成了A,另一个线程使用CAS检查时会发现共享变量值并未变化,实际上是变化了。对于数值类型的变量,比如int,这种问题关系不大,但对于引用类型,则会产生很大影响。
解决思路:在变量前加版本号,每次变量更新时将版本号加1,A -> B -> A,就变成 1A -> 2B -> 3A。
JDK5之后Atomic包中提供了AtomicStampedReference#compareAndSet来解决ABA问题。
2. 长时间自旋非常消耗资源
先说一下什么叫自旋,自旋就是cas的一个操作周期,如果一个线程特别倒霉,每次获取的值都被其他线程的修改了,那么它就会一直进行自旋比较,直到成功为止,在这个过程中cpu的开销十分的大,所以要尽量避免。
3. 只能保证一个共享变量的原子操作
CAS只能对单个共享变量执行操作,对多个共享变量操作时则无法保证原子性,此时可以用锁。或者将多个共享变量合成一个共享变量来操作。比如a=2,b=t,合并起来ab=2t,然后用CAS操作ab。
JDK5提供AtomicReference保证引用对象间的原子性,它可将多个变量放在一个对象中来进行CAS操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值