Java多线程 一

156 阅读8分钟

1 什么是进程和线程

  • 进程:进程是指内存中的一个应用,比如我们的jar包,亦或者游戏,在linux或者windows都是一个进程
  • 线程:线程是指进程中的一个执行流程,一个进行可以有多个线程,比如吃鸡游戏中每个玩家都是一个线程,线程隶属于进程

2Java中的线程的实现

Java中创建一个线程有2中实现方法

  • 继承Thread
  • 实现Runnable 接口

2.1 继承Thead

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() +" "+i);
        }
    }
}

public class MyThreadTest {
    public static void main(String[] args) {
        MyThread th1 = new MyThread();
        MyThread th2 = new MyThread();
        th1.start();
        th2.start();
    }
}

得到输出

Thread-0 0
Thread-1 0
Thread-0 1
Thread-1 1
Thread-1 2
Thread-1 3
Thread-0 2
Thread-0 3
Thread-1 4
Thread-0 4

2.2 实现Runnable

public class MyThread2 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+" 当前是 "+ i);
        }
    }
}

public class MyThreadTest {
    public static void main(String[] args) {
        MyThread2 myThread1 = new MyThread2();
        MyThread2 myThread2 = new MyThread2();
        Thread thread = new Thread(myThread1);
        Thread thread2 = new Thread(myThread2);
        thread.start();;
        thread2.start();
    }
}
Thread-0 当前是 0
Thread-1 当前是 0
Thread-0 当前是 1
Thread-1 当前是 1
Thread-0 当前是 2
Thread-1 当前是 2
Thread-0 当前是 3
Thread-0 当前是 4
Thread-1 当前是 3
Thread-1 当前是 4
Thread-1 当前是 5
Thread-0 当前是 5
Thread-1 当前是 6
Thread-0 当前是 6
Thread-1 当前是 7

2.3 注意点

1.run 方法只是一个普通的方法,start才会让线程启动,但是start 也不回立马运行线程,而是让线程处于可执行状态,当该线程得到机会执行时就会运行 run方法 2. 每个线程都有自己的名字, 我们可以通过Thread.currentThread().getName() 来获取

3 线程状态的切换

3.1 五个状态

  • 新建:线程对象已经创建,但是还没调用 start 方法
  • 可运行: 有2种可能:
    • 线程已经调用start 方法,但是调用程序还没将选定该线程并允许
    • 线程运行之后从阻塞或者休眠,等待 状态恢复到 可运行
  • 运行: 线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
  • 等待/阻塞/休眠 : 线程是有资格运行的,但是基于某些外力因素,其暂停运行,当某些条件达到时就会赞词变成可运行状态
  • 死亡 : 线程执行完就任务是死亡,如果在死亡的线程上调用start,会抛出异常

3.2 线程状态机

3.3 几个阻止线程允许的方法

  • sleep方法 Thread.sleep(long millis)Thread.sleep(long millis, int nanos) 强制让运行中的线程进入休眠状态,当休眠时间到达时,线程回到可运行状态,但不是直接运行哦, 该方法是静态方法,只能控制当前运行的线程, -yield 该方法可以暂停当前运行的线程,并执行其他线程, 但是实际上,yield 并不能保证达到让步的目的,即便某个线程进行让步操作,但是它还是会被线程调度再次选中
  • join 非静态方法,可以让让一个线程B加入到另外一个线程A的尾部。在B执行完毕之前,A不能工作

4 线程的同步

线程的同步是为了防止同一个变量被多个线程同时访问,造成意想不到的后果,为了保证线程安全,我们可以采取同步锁机制

4.1 同步锁的原理

Java中每一个对象都可以作为锁

  1. 普通同步方法,锁住的是实例对象
  2. 同步代码块, 锁住的是括号里的对象
  3. 静态同步方法,锁住的是当前class

当一个线程访问同步代码块或者同步方法的时候,需要现获取到锁

  • 同步代码块的锁是依赖 montitorEnter 和 monitorExit 来实现的,JVM需要保证每个同步代码块都有对应的 enter 和exit,enter标志着线程持有同步代码块,即获得对象的同步锁,exit标志放弃同步代码块
  • 同步方法时依赖方法修饰符的 ACC_SYNCHRONIZED 实现的

4.2 对象头

Java中的对象头是实现同步锁的基础,我们下面就简单了解一下。 对象头分2部分数据,Mark word(标记文字) 和Klass Point (类型指针)

mark word用于存储对象运行是的一些数据,包括GC分代年龄,锁状态标志, hashcode,线程持有的锁等等,它是一个非固定的数据结构,以便以最小的空间内存存储最多的数据 monitor 是一种同步机制,通常也被描述成一个对象。monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下:

4.4 锁优化

4.4.1 自旋锁

自旋锁的引进主要是为了解决CPU频繁的切换状态(也是因为对象的锁只会维持一段比较短的时间)。所谓自旋锁就是让线程多等待一段时间(通过一段无意义的循环),而不是立刻挂起。

  • 优点: 免线程切换带来的开销
  • 缺点: 如果持有锁的线程一段时间内没有释放,那就表示自旋的线程就会白白消耗掉处理的资源。自旋等待的时间(自旋的次数)必须要有一个限度

4.4.2适应自旋锁

自旋锁可以人为控制自旋的次数,但是如果我们刚刚结束自旋进入挂起状态,之前占茅坑的出来,岂不是很尴尬。那么自适应自旋锁 就应运而生了。自旋的次数不再是固定的。而是由上一次同一个锁的释放时间和上一个持有者的状态决定的。如果线程自旋成功,下次自旋次数就会增加(既然上一次成功,这一次也会成功)。如果自旋失败,下次自旋次数就会降低。

4.4.3 锁消除

JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除.

4.4.4 锁粗化

锁的粒度越小越好吗?大部分情况下我是认同的,但是如果一系列的加锁解锁会导致频繁的CPU切换。会带来很多不必要的性能损耗。锁粗化就是将一连串的加锁,释放锁链接成一个大锁

4.4.5 轻量级锁

没有多线程竞争的前提下,为了减少传统的重量级锁使用操作系统互斥量产生的性能消耗,当关闭偏向锁或者多个线程竞争偏向锁,偏向锁就会升级为轻量级锁。

4.4.5.1 获取轻量级锁步骤如下
  1. 判断当前对象是否处于无锁状态。若是JVM则会从当前线程的栈帧中开辟出一片名为锁记录(Lock Record)的内存,并将当前对象的 Mark Word 存储到锁记录中。否则执行步骤3
  2. JVM利用CAS 尝试将对象的 Mark word 更新为指向Lock Record的栈帧,如果成功,表示获取到轻量级锁,否则执行步骤3
  3. 判断对象的Mark Word 是否指向当前线程的栈帧,如果是表示当前线程已经持有轻量级锁,进入同步代码块执行,反之对象的锁已经被其他线程抢占,轻量级锁膨胀微重量级锁
4.4.5.2 释放轻量级锁
  1. 去除轻量级锁在 Lock Record 中保存的数据
  2. 用CAS 尝试将取出的数据替换成为对象的Mark Word 中,如成功,表示释放成功,反之执行 步骤3
  3. 如果失败,说明有其他线程尝试获取锁,需要在释放锁的同事唤起其他线程

轻量级锁的主要理论依据是对于绝大部分的锁,其整个生命周期内都是不会存在竞争的。如果在高并发情况下,轻量级锁的性能比重量级锁还要慢(因为引入了CAS)

4.4.6 偏向锁

偏向锁在无多线程竞争的情况下尽量减少不必要减少了CAS的操作

4.6.1 获取锁
  1. 检测对象的 Mark Word是否为可偏向状态,偏向锁1,锁标识位为01;
  2. 若是可偏向状态,且线程ID为当前线程ID,则执行步骤5,反之执行步骤3
  3. 若线程ID != 当前线程ID,则通过CAS自旋将 对象的Mark Word 中的线程ID更新为当前线程的ID,执行失败则进入步骤4
  4. CAS竞争锁失败,证明当前存在多线程竞争情况,竞争线程被挂起,升级为轻量级锁
  5. 执行同步代码

4.5 注意事项

Java中常说的同步锁其实就是对象锁,了解JVM运行机制的小伙伴肯定都知道,Java的每个对象头都有一个锁,当运行到非静态 的 synchronized 修饰的方法或者同步代码块的时候,该线程会自动获取当前对象的锁,因为一个对象只有一把锁,A线程占有锁并且未释放的前,其他线程都无法站有所。下面综合总结了一些锁和同步的要点

  1. 每个对象只有一把锁,被 synchronized 修饰的非静态方法或者 同步代码块同一时间只能运行一个线程执行。
  2. 对象可以同时拥有同步方法和非同步方法,非同步方法不会产生竞争和锁占用
  3. 线程休眠时,其持有的锁不会被释放
  4. 一个线程可以同时拥有不同对象的多个 同步锁
  5. 静态方法同步 锁住的是 class,即便是不同的对象实例之间也会产生竞争