Java多线程(四)

并发理论

Java内存模型
垃圾回收有什么目的,什么时候进行垃圾回收
垃圾回收是在内存中存在没有引用的对象或者超过作用域的对象时进行的
目的是识别并且丢弃应用不再使用的对象来释放和重用资源。

如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存
不会,在下一个垃圾回调周期中,这个对象将是被可回收的,也就是说并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存。

finalize()方法什么时候被调用?析构函数(finalization)的目的是什么
1.垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法;finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize()throws Throwable{}。在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其资源的回收。

注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间。

2.GC是内存回收,为何还需要finalization呢,大部分情况下,什么都不需要做(不需要重载)。只有在某些很特殊的情况下,比如调用了一些native的方法(一般C方法),可以要在finalization里去调用C的释放函数。
native就是Java调用非Java接口(比如调用C或C++)。

重排序与数据依赖性
为什么代码会重排序

在执行程序时,为了提升性能,处理器和编译器常常会对指令进行重排序,但是不能随意进行重排序,需要满足以下两个条件
1.在单线程环境下不能改变程序运行的结果
2,存在数据依赖关系的不允许重排序

注意:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。

as-if-serial规则和happens-before规则的区别
1.as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。

2.as-if-serial语义给编写单线程程序的程序员创造了一个环境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个环境:正确同步的多线程程序是按happens-before指定的顺序来执行的。

3.as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

并发关键字
synchronized
synchronized的作用
synchronized关键字是用来控制线程同步的,在多线程环境下,控制synchronized代码段不被多个线程同时执行。synchronized可以修饰类、方法、变量。

在早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层操作系统的Mutex Lock来实现的,Java的线程是映射到操作系统的原生线程上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的切换需要相对较长的时间,时间成本较高,这也是早期synchronized效率低的原因。
在Java6之后从JVM层面对synchronized进行较大优化,现在的synchronized锁效率也很高。JDK1.6对锁的实现引入了大量优化操作,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

怎么使用synchronized关键字,项目中用到了吗
synchronized关键字最主要的三种使用方式:
1.修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。
2.修饰静态方法:给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static表明这是该类的一个静态资源,不管new多少个对象,只有一份)。因此,如果一个线程A调用一个实例对象是非静态synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象,因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized方法占用的是当前实例的锁。
3.修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。

总结:synchronized关键字加到static静态方法和synchronized(class)代码块上都是给class类加锁。synchronized加到实例方法上是给对象实例加锁。尽量不要使用synchronized(String a)因为JVM中,字符串常量池具有缓存功能。

单例模式了解吗,实现一下,解释一下双重检验锁方实现单例模式的原理。
双重校验锁实现对象单例(线程安全):

public class Singleton{
	private volatile static Singleton uniqueInstance;
	private Singleton(){
	}
	public static Singleton getUniqueInstance(){
		//先判断对象是否已经实例过,没有实例化过才进入加锁代码
		if(uniqueInstance==null){
			//类对象加锁 此时可能有多个线程同时进入
			synchronized(Singleton.class){
				//再次判断对象是否已经实例过
				if(uniqueInstance==null){
					uniqueInstance=new Singleton();
				}	
			}
		}
		return uniqueInstance;
	}
}

注意:uniqueInstance需要采用volatile关键字修饰
uniqueInstance=new Singleton();分三步执行
1.为uniqueInstance分配内存空间
2.初始化uniqueInstance
3.将uniqueInstance指向分配的内存地址

为什么需要判断两次uniqueInstance
1.第一次在同步块外判断uniqueInstance是否已经实例化是为了避免读操作频繁加锁。
2.同步块内部判断uniqueInstance是否已经实例化时是因为在同一时刻可能存在多个线程已经执行过第一个if判断,因此需要通过同步块内部的if判断uniqueInstance是否实例化,避免多个线程创建多个实例。
总结:
1.同步块外判断是为了防止读操作每次进行getUniqueInstance()都会进行加锁动作,对于已经实例化的就不需要加锁,而未实例化的才需要加锁
2.同步块内加锁是因为此时可能已经有多个线程同时执行了外部判断,进入同步块,此时,如果不加判断,那么多个线程会先后实例化,加了实例化后,在第一个线程实例化后,第二个线程会先判断是否已经实例化,若已经实例化则不实例化。

synchronized底层实现原理
synchronized是Java中的一个关键字,在使用的过程中并没有看到显示的加锁和解锁过程。因此有必要通过javap命令,查看相应的字节码文件
synchronized同步语句块的情况

public class SynchronizedDemo{
	public void method(){
		synchronized(this){
			S.o.p("synchronized 块")
		}
	}
}

为什么会有两个monitorexit
防止同步代码块中线程因异常退出,而锁没有释放,必然造成死锁(等待的线程永远得不到锁)。因此最后一个monitorexit是保证异常情况下,锁也可以释放,避免死锁

仅有ACC_SYNCHRONIZED这个标志,该标志表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit

synchronized可重入原理
重入锁是指一个线程获取该锁后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获取该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁

什么是自旋
很多synchronized里面的代码只是一些简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,可以让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,就是自旋。如果做了多次循环发现还没有得到锁,再阻塞,这样可能是一种更好的策略。

多线程中synchronized锁升级原理
synchronized锁升级原理:
在锁对象的对象头里有一个threadid字段,在第一次访问时threadid为空,jvm让其持有偏向锁,并将threadid设置为线程id,再次进入时会先判断threadid是否与其线程id一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数后,还没有正常获得要使用的对象,此时就会把锁从轻量级锁升级为重量级锁,此过程就构成了synchronized锁的升级。
锁升级的目的:减低锁带来的性能消耗。在Java6之后优化synchronized实现方式,使用偏向锁升级为轻量锁再升级为重量锁的方式,减低锁带来的性能消耗。
轻量级锁:
优点:
竞争的线程不会阻塞,提高了程序的相应素的
缺点:
如果始终得不到锁竞争的线程使用自旋会消耗性能
适用场景:
追求响应时间,同步块执行速度非常快
重量级锁:
优点:
线程竞争不使用自旋,不会消耗CPU
缺点:
线程可能阻塞,响应时间慢。
适用场景:
追求吞吐量。同步块执行速度较长

偏向锁:
优点:
加锁和解锁不需要额外的消耗,和执行异步方法比仅存在纳米级差距
缺点:
如果线程间存在锁竞争,会带来额外的锁撤销的消耗
适用场景:
适用于只有一个线程访问同步块场景

线程B如何知道线程A修改了变量
1.volatile修饰变量
2.synchronized修饰修改变量的方法
3.wait/notify
4.while轮询

当一个线程进入一个对象的synchronized方法A之后,其它线程是否可以进入此对象的synchronized方法B
不能。其它线程只能访问该对象的异步方法,同步方法不能进入。
因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(不是等待池)中等待A方法执行完释放锁,然后再重新获取锁再进入B方法。

synchronized、volatile、CAS比较
1.synchronized是悲观锁,属于抢占式,会引起其它线程阻塞
2.volatile提供多线程共享变量可见性和禁止指令重排序优化
3.CAS是基于冲突检测的乐观锁(非阻塞)

synchronized和Lock的区别
1.synchronized是内置关键字,在JVM层面,Lock是一个类
2.synchronized可以给类、方法、代码块加锁;而Lock只能给代码块加锁
3.synchronized不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;Lock需要自己加锁和释放锁,如果使 用不当没有unlock()去释放锁就会造成死锁。
4.通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

synchronized和ReentrantLock的区别
1.synchronized是关键字,ReentrantLock是类
2.ReentrantLock是类,它提供了比synchronized更多更灵活的特性,它可以被继承、可以有方法、可以有各种类变量
相同点:
两者都是可重入锁

可重入锁概念:自己可以再次获取自己的内部锁。
比如一个线程获得了某个对象的锁,此时这个对象的锁还没有释放,当其再次想要获取这个对象的锁的时候,还是可以获取的,如果不可重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器自增1,所以要等到锁的计数器下降为0时才能释放锁。

主要区别:
1.ReentrantLock使用起来灵活,但是必须有释放锁的配合动作
2.ReentrantLock必须手动获取与释放锁,而synchronized不需要手动释放锁
3.ReentrantLock只适用于代码块锁,而synchronized可以修饰方法、类、变量等
4.锁机制不同。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的是对象头中的mark word

每一个对象都可以作为锁,这是synchronized实现同步的基础:
1.普通同步方法,锁是当前实例对象
2.静态同步方法,锁是当前类的class对象
3.同步代码块,锁是括号内的对象

可重入锁有什么作用
可重入锁也叫递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然可以获取该锁的代码,但不受影响。
作用及使用场景:
1.可重入锁的最大作用是避免死锁
2.当一个线程执行一个带锁的代码块或方法,同时代码块或方法里也获取同一个锁。为了避免死锁,此时可以使用可重入锁。

ex:synchronized重入锁

public class SynchronizedSample implements Runnable{
	public synchronized void funA(){//执行funA方法,需要获取对象锁,synchronized属于可重入锁,进入funA方法后,拥					  有对象锁,所以执行funB方法时不受影响
		S.o.p(Thread.currentThread().getName()+""+Thread.currentThread.getId());
		funB();
	}
	public synchronized void funB(){//执行funB方法,需要获取对象锁
		S.o.p(Thread.currentThread().getName()+""+Thread.currentThread.getId());
	}
	@override
	public void run(){
		funA();
	}
	public static void main(String[] args){
		SynchronizedSample ss=new SynchronizedSample();
		new Thread(ss).start(); 
		new Thread(ss).start();
		new Thread(ss).start();
	}
}

ex:ReentrantLock

public class ReentrantLockSample implements Runnable{
	ReentrantLock lock=new ReentrantLock();
	public void funA(){//执行funA方法,需要获取对象锁
		lock.lock();//第一次获取锁
		S.o.p(Thread.currentThread().getName()+""+Thread.currentThread.getId());
		funB();//该方法第二次获取锁
		lock.unlock();
	}
	public funB(){//执行funB方法,需要获取对象锁
		lock.lock();
		S.o.p(Thread.currentThread().getName()+""+Thread.currentThread.getId());
		lock.unlock();
	}
	@override
	public void fun(){
		funA();
	}
	public static void main(String[] args){
		ReentrantLockSample rs=new ReentrantLockSample();
		new Thread(rs).start(); 
		new Thread(rs).start();         
		new Thread(rs).start();					
	}							   
}

创建多个线程方式1:

for(i=0;i<3;i++){
	new Thread(new Runnable(){
		@override
		public void run(){
			//S.o.p();
		}
	}).start();
}

创建多个线程方式2:

for(i=0;i<3;i++){
	new Thread(rs).start();
}

执行结果:
Thread-1 14
Thread-1 14
Thread-2 15
Thread-2 15
Thread-0 13
Thread-0 13

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值