Java多线程高并发之基础概念篇

一. 基础概念

  • 什么是线程?和进程的区别?
    一个程序相当于一个进程,而这个程序中不同的执行路径就是线程。
  • 启动线程的几种方式
    1:Thread 2: Runnable 3:Executors.newCachedThrad
  • 创建线程的简单示例:
public class T02_HowToCreateThread {
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Hello MyThread!");
        }
    }
    static class MyRun implements Runnable {
        @Override
        public void run() {
            System.out.println("Hello MyRun!");
        }
    }
    public static void main(String[] args) {
        new MyThread().start();
        new Thread(new MyRun()).start();
        new Thread(() -> {
            System.out.println("Hello Lambda!");
        }).start();
    }
}

二. 线程最基本的方法

  • Thread.sleep(): 当前线程暂停一段时间,sleep完之后线程进入就绪状态;
    使用场景:将CPU让给别的线程去运行
  • Thread.yield(): 暂时退出进入等待队列,让别的线程执行,让出一下CPU,不能保证别的线程拿到资源;
    使用场景:做性能测试时压测可能用到
  • t.join(): 在当前线程A调用线程B的join方法,当前线程A等待线程B执行完才执行;
    使用场景:保证多个线程按顺序执行
  • t.interrupt(): 中断当前线程,抛出InterruptedException异常,程序决定如何处理InterruptedException异常,一般不会用interrupt控制业务逻辑; 使用场景:如果某个线程一直处于等待状态比如sleep2天,此时可以调用interrupt()中断当前线程
  • t.stop(): 终止线程,一般很少用

三. 线程常用的状态

一个线程从创建到消亡可能经过的状态有哪些?

  • New;
  • Runnable(ready,running);
  • TimedWaiting;
  • Waiting;
  • Blolcked;
  • Terminated(结束)
    状态之间如何流转?请看下面的状态流程图,是不是一目了然
    线程状态转换流程

四. synchronized与volatile的区别

4.1 区别

  • volatile主要应用在多个线程对实例变量更改的场合,刷新主内存共享变量的值从而使得各个线程可以获得最新的值,线程读取变量的值需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。另外,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中(即释放锁前),从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞,比如多个线程争抢synchronized锁对象时,会出现阻塞。
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性,因为线程获得锁才能进入临界区,从而保证临界区中的所有语句全部得到执行。
  • volatile标记的变量不会被编译器优化,可以禁止进行指令重排;synchronized标记的变量可以被编译器优化。

volatile的作用

  • 保证线程可见性(MESI CPU的缓存一致性协议,保证变量值在多个线程中一致)
    禁止指令重排序(编译时分三步:1申请一块内存(并赋默认值比如int=0),2初始化修改默认值,3引用赋值最终初始化,多线程下存在指令重排情况,添加volatile后禁止指令重排序(CPU),等对象初始化完成后才赋值)
    volatile实现线程安全的单例模式:
public class TestSingleton01 {
    // volatile 作用:1 保证线程可见性(MESI,缓存一致性协议),2 禁止指令重排序
    // volatile 不能替代synchronized,它不能保证原子性
    // JIT 超高并发场景,这里需要添加volatile,因为多线程下存在指令重排情况,添加volatile后禁止指令重排序(CPU),等对象初始化完成后才赋值
    private static volatile TestSingleton01 instance;
    public TestSingleton01(){
    }
    public static TestSingleton01 getInstance(){
        // 业务逻辑代码省略
        if(instance == null){
            synchronized (TestSingleton01.class){
                // 双重检查
               if(instance == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new TestSingleton01();// 指令分为三步:1申请一块内存(并赋默认值比如int=0),2初始化修改默认值,3引用赋值instance(最终初始化)
               }
           }
        }
        return instance;
    }
}

volatile是否能代替synchronized?
不能,volatile不能保证原子性操作,运行以下程序查看其结果:

public class T04_VolatileNotSync {
	volatile int count = 0;
	void m() {
		for(int i=0; i<10000; i++) count++;
	}	
	public static void main(String[] args) {
		T04_VolatileNotSync t = new T04_VolatileNotSync();		
		List<Thread> threads = new ArrayList<Thread>();		
		for(int i=0; i<10; i++) {
			threads.add(new Thread(t::m, "thread-"+i));
		}		
		threads.forEach((o)->o.start());		
		threads.forEach((o)->{
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});		
		System.out.println(t.count);		
	}	
}

以上程序运行结果永远不等于100000,为什么?因为volatile不能保证原子性,需要在m方法上加上synchronized。

总结:
synchronized 是给对象上锁,保证其在多线程中的原子性。
volatile 修饰变量,保证其在多线程中的可见性。

4.2 synchronized实现原理
先思考一个问题:
synchronized锁的对象是什么?

  • 是一个Object对象(头两位),不能是String="object"常量,及Long,Integer(如果类库中锁了一个常量,多个地方用到这个常量时,就会存在死锁阻塞问题,而对象不会)
    ; 注意:在静态方法上加synchronized
    相当于synchronized(T.class),在方法内部写synchronized(this)和在方法上加synchronized是等同的(T.class
    理论上是单例的,如果在同一个classLoader中)
    锁定某对象o,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变,
    应该用final修饰避免将锁定对象的引用变成另外的对象。

通过以下代码验证:

public class T {	
	private static volatile int count = 10;
	private final Object o = new Object();	
	public void m() {
		synchronized(o) { //任何线程要执行下面的代码,必须先拿到o的锁
			count--;
			System.out.println(Thread.currentThread().getName() + " count = " + count);
		}
	}
	public static void main(String[] args) {
		new Thread(()->{
			T t= new T();
			t.m();
		}).start();
        //创建第二个线程
        Thread t2 = new Thread(t::m, "t2");		
        t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会		
        t2.start();
	}	
}

synchronized 实现原理(锁升级)
最初的jdk版本中是重量级锁,每次都需要向操作系统申请锁,1.5后hotspot进行了改进:
synchronized(Object)
1)mark word记录线程id,并标记线程锁状态为:偏向锁(下次遇到同样的线程时直接返回当前锁)
2)如果存在锁争用的情况,其它线程争用这把锁时,升级为:自旋锁(CAS第二个线程等着第一个结束,等待中的线程占用CPU)
3)自旋10次之后还得不到锁时,升级为:重量级锁(OS-等待中的线程不占用CPU)
思考什么情况下用自旋锁,什么情况下用重量级锁(系统锁)?
1)加锁代码执行时间短,线程数少时用【自旋锁】
2)加锁代码执行时间长,线程数多时用【系统锁】
synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
底层实现:JVM没有特殊要求,拿出对象头部信息Mark Word(标记字)的2两位作为来描述不通类型的锁,另外Mark Word还可以用来配合GC、存放该对象的hashCode:
在这里插入图片描述
以上是Java对象处于5种不同状态时,Mark Word中64个位的表现形式,上面每一行代表对象处于某种状态时的样子。其中各部分的含义如下:
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_lock和lock一起,表达的锁状态含义如下:
在这里插入图片描述
延申知识对象的结构:我们都知道,Java对象存储在堆(Heap)内存。那么一个Java对象到底包含什么呢?概括起来分为对象头、对象体和对齐字节。如下图所示
在这里插入图片描述
对象的几个部分的作用:
1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;
2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;
3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;
4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;
5.对齐字是为了减少堆内存的碎片空间(不一定准确)。

五. Java原子类AtomicXXX

AtomicXXX类型解决原子性问题的更高效的方法,使用AtomXXX类,AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的
实现原理:
通过Unsafe的compareAndSwap这类原子性的方法实现,分配内存和改变类的属性
采用CAS(Compare And Set 属于CPU原语指令,执行过程不会被打断)

cas(Value,Expected,NewValue)
- if Value == Expected
Value =NewValue
otherwise try again or fail

ABA问题
在修改对象属性之前,别的线程对属性值进行了修改,比如原来值为A ,别的线程将值改成了B,又改成了A;这时CAS操作就会误认为它是从来没被改变过;会造成对象的属性值引用不一样的问题,可能造成程序逻辑问题
如果解决?
JUC包提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以控制变量值的版本来保证CAS的正确性, 增加version :A1.0 B-2.0 A3.0; cas(version),如果是基础数据类型不要管,引用类型加版本。
Unsafe类
13版本的jdk可用其直接操作内存中的实例的属性

import sun.misc.Unsafe;
public class HelloUnsafe {
    static class M {
        private M() {}
        int i =0;
    }
   public static void main(String[] args) throws InstantiationException {
        Unsafe unsafe = Unsafe.getUnsafe();
        M m = (M)unsafe.allocateInstance(M.class);
        m.i = 9;
        System.out.println(m.i);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值