Java多线程

1、使用synchronized关键字的使用方式:

(1)修饰实例的方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁;
(2)修饰静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁,即给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static表明该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁),所以如果一个线程A调用一个实例对象的非静态synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法是允许的,不会出现互斥现象,因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized方法占用的是当前实例对象的锁。
(3)修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块前需要获得给定对象的锁,和synchronized方法一样,synchronized(this)代码块也是给定当前对象的,synchronized关键字加到static静态方法和synchronized(class)代码块上都是给class类加锁,synchronized关键字加到非静态方法上是给对象实例上锁,尽量不要使用synchronized(String a),因为JVM中,字符串常量池具有缓冲功能。
eg:

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();
					/*上一步需要分为三个步骤:
					1、为uniqueInstance分配内存空间;
					2、初始化uniqueInstance;
					3、将uniqueInstance指向分配的内存地址
					*/
				}
			}
		}
		retrun uniqueInstance;
	}	
}

使用volatile可以禁止JVM的指令重排,保证再多线程环境下也能正常进行
2、synchronized关键字的底层原理
(1)synchronized同步语句块的情况

public class SynchronizedDemo{
	public void method(){
		synchronized(this){
			System.out.println("synchronized代码块");
		}
	}
}

synchronized同步语句块的实现使用monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令指向同步代码块的结束位置,当执行monitorenter指令时,线程试图获取锁,即获取monitor(monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,即Java中任何对象可以作为锁的原因)的持有权,当计数器为0则可以成功获取,获取后将锁计数器+1,相应的再执行monitorexit指令后,将锁计数器设为0,表明锁被释放,弱获取对象锁失败,当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
3、synchronized修饰方法

public class SychronizedDemo2{
	public synchronized void main(){
		System.out.println("synchronized方法");
	}
}

synchronized修饰方法有ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该访问标识来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
3、JDK1.6之后的synchronized关键字底层的优化及优点
JDK1.6之后对锁的实现引入大量优化,如无锁、偏向锁、轻量级锁、重量级锁,这些锁可以升级不可以降级,目的是为了提高获得锁和释放锁的效率。
偏向锁:为了在没有多线程的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,轻量级锁在无竞争的情况下使用CAS操作去代替使用互斥量,而偏向锁在无竞争的情况下会把整个同步都消耗掉。偏向锁会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,则持有偏向锁的线程就不需要进行同步。
若锁竞争比较激烈,偏向锁则失效,这种情况下极有可能每次申请锁的线程都不相同,这时不能使用偏向锁,否则得不偿失,偏向锁失效后并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
轻量级锁:在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量,轻量级锁的加锁和解锁都用到CAS操作。
轻量级锁能够提升程序同步性的依据:对于绝大部分锁,在整个同步周期内都不存在竞争,如果没有竞争,轻量级锁使用CAS操作避免了使用互斥操作的开销,但如果有锁竞争,除了互斥量开销外还会发生CAS操作,因此有锁竞争的情况下,轻量级锁比传统的重量级锁更慢,如果锁竞争激烈,则轻量级将很快膨胀为重量级锁。
4、自旋锁和自适应自旋锁
轻量级锁失败后,虚拟机为了避免线程真实的在操作系统层面挂起,还会进行一项称为自旋锁的优化手段,互斥同步对性能最大的影响是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核中完成(用户态转换到内核态会耗费时间)。为了避免在短时间内让线程挂起,则让线程执行一个忙循环(自旋)。
互斥锁:如果资源已被占用,资源申请者只能进入睡眠状态。
自旋锁:不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者一直循环看该自旋锁保持者是否已释放锁。
自适应自旋锁:自旋时间不再固定,根据前一次同一个锁上的自旋时间及锁的拥有者状态来决定。
锁消除:虚拟机在编译器运行时,若检测到那些共享数据不可能存在竞争,则执行锁消除,节省毫无意义的请求锁时间。
锁粗化:编写代码时,推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域才进行同步,即为了使得需要同步的操作数量尽可能小,若存在锁竞争,则等待线程也能尽快拿到锁。

Synchronized和ReenTrantLock的对比:

(1)两者都是可重入锁:可重入锁自己可以再次获取自己的内部锁,如一个线程获得了某个对象的锁,此时这个对象还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话会造成死锁,通过一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
(2)synchronized依赖于JVM而ReenTrantLock依赖于API
synchronized依赖于JVM实现,对其优化也是在虚拟机层面实现的,并未直接暴露给我们,ReenTrantLock时JDK层面的(API层面需lock()和unlock()配给try{}finally{}语句块实现);ReenTrantLock的高级功能:
(1)等待可中断;
(2)可实现公平锁;
(3)可实现选择性通知
ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()实现,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReenTrantLock可以指定时公平锁还是非公平锁,而synchronized只能时非公平锁;公平锁是无等待的线程先获得锁,ReenTrantLock是非公平的,可通过ReenTrantLock类的ReentrantLock(boolean fair)构造方法来指定是否是公平的。
synchronized关键字与wait()和notify/notifyAll()方法相结合可实现等待/通知机制,ReenTrantLock类需借助Condition接口与newCondition()方法实现,Condition可实现多路通知功能,即在一个Lock对象中可创建多个Condition实例(对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。使用notify/notifyAll()方法进行通知时,被通知的线程是由JVM选择的,用ReenTrantLock类结合Condition实例可实现“选择性通知”;
synchronized关键字相当于整个Lock对象中只有一个Condition实例,所有线程都注册在它一个身上,如果执行notifyAll()方法就会通知所有处于等待状态的线程,造成很大效率问题,而Condition实例的singalAll()方法只会唤醒注册在该Condition实例中的所有等待线程。

Java内存模型

JDK1.2之前,Java内存模型实现总是从主存(即共享内存)读取变量,当前Java内存模型,线程可以把变量保存在本地内存(eg:机器的寄存器)中,而不是直接在主存中进行读写,可能造成一个线程在主存中修改了一个变量的值,而另一个线程还继续使用它的寄存器中的变量值的拷贝,造成数据的不一致。
将变量声明为volatile,指示JVM这个变量是不稳定的,每次使用它都到主存中进行读取。
volatile:保证变量可见性,防止指令重排序

volatile与synchronized关键字比较:

(1)volatile关键字是线程同步的轻量级实现,则性能比synchronized关键字更好,但volatile关键字只能用于变量而synchronized关键字可修饰方法及代码块;synchronized关键字是为了减少获得锁和释放锁带来的性能消耗,引入的偏向锁和轻量级锁及其它各种优化之后执行效率有了显著提升。
(2)多线程访问volatile关键字不会发生阻塞,而synchronized可能发生阻塞;
(3)volatile可保证数据可见性,不能保证数据原子性;synchronized既可以保证数据可见性,也能保证数据的原子性;
(4)volatile主要用于解决变量在多个线程之间的可见性,synchronized解决的是多个线程之间访问资源的同步性。

为什么要用线程池?

线程池提供了一种限制和管理资源,每个线程池还维护一些基本统计信息;
优点:
(1)降低资源消耗;通过重复利用已创建的线程,降低线程创建和销毁造成的消耗;(2)提高响应速度,当任务到达时不需要等到线程创建就能立即执行;
(3)提高线程的可管理性;
(4)线程是稀缺资源,若无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行同一的分配调优和监控。

实现Runnable接口和Callable接口的区别:

线程池执行任务需要实现的接口,Runnable接口或Callable接口的实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行;
区别:Runnable接口不会返回结果但Callable接口可以返回结果;
工具类:Executor可以实现Runnable对象和Callable对象之间的相互转换;

执行execute()方法和submit()方法的区别是什么呢?

(1)execute()方法用于提交不需返回值的任务,所以无法判断任务是否被线程池执行成功。
(2)submit()方法用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过future对象可以判断任务是否执行成功,且可以通过future的get()方法获取返回值,get()方法会阻塞当前线程直到任务完成,使用get(long tomeout,TimeUnit unit)方法会阻塞当前线程一段时间后立即返回,这时有可能任务没有执行完成。

如何创建线程池?

不使用Executors创建,而是通过ThreadPoolExecutor方式,更加明确线程池的运行规则,规避资源耗尽的风险,
Executors返回线程池对象的弊端:
FixedThreadPool和SingleThreadExecutor:允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量请求,从而导致OOM;
CachedThreadPool和ScheduleThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM;
创建线程方式一:通过构造方法实现:
在这里插入图片描述
方式二:通过Executor框架的工具类Executor来实现,创建三种ThreadPoolExecutor.
(1)FixedThreadPool:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,当有一个新任务提交时,线程池中若有空闲线程,则立即执行;若没有,则新的任务会被暂存在一个任务队列中,当有线程空闲时,便处理在任务队列中的任务。(2)SingleThreadExecutor:方法返回一个只有一个线程的线程池,若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,当线程空闲时,按先入后出的顺序执行队列中的任务。
(3)CachedThreadPool:返回一个可根据实际情况调整线程数量的线程池,线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程,若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务,所有线程在当前任务执行完毕后,将返回线程池进行复用。

Atomic原子类

Atomic原子类:具有原子操作特征的类;
Atomic:指一个操作是不可中断的,即使多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
JUC包中的4类原子类
基本类型(使用原子方式更新基本类型):
AtomicInteger:整型原子类
AtomicLong:长整型原子类
AtomicBoolean:布尔型原子类
数组类型(使用原子方式更新数组量的某个元素):
AtomicIntegerArray:整型数组原子类
AtomicLongArray:长整型数组原子类
AtomicReferenceArray:引用类型数组原子类
引用类型
AtomicReference:整型数组原子类
AtomicStampedReference:长整型数组原子类
AtomicMarkableReference:引用类型数组原子类
对象的属性修改类型
AtomicIntegerFieldUpdater:原子更新整型字段的更新器
AtomicLongFieldUpdater:原子更新长整型字段的更新器
AtomicStampedReference:原子更新带有版本号的引用类型
AtomicStampedReference:该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。

AtomicInteger常用方法

public final int get()  //获取当前值
public final int getAndSet(int newValue)  //获取当前值并设置新的值
public final int getAndIncrement()   //获取当前值并自增
public final int getAndDecrement()  //获取当前值并自减
public final int getAndAdd()   //获取当前值并加上预期值
boolean compareAndSet(int except,int update)  //如果输入的值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)  //最终设置为newValue,使用lazySet设置之后可能导致其他线程之后一小段时间还可读到旧值

AtomicInteger类的使用示例:使用AtomicInteger之后,不用对increment()方法加锁也可以使用线程安全

class AtomicIntegerTest{
	private AtomicInteger count = new AtomicInteger();
	public void increment(){
		count.incrementAndGet();
	}
	public int getCount(){
		return count.get();
	}
}

AtomicInteger类原理:

//更新操作时提供“比较并替换”的作用
private static final unsafe = unsafe.getUnsafe();
private static final long valueOffset;
static{
	try{
		valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
	}catch(Exception ex){
		throw new Error(ex)}
}
private volatile int(value);

AtomicInteger类主要利用CAS(compare and swap)+volatile和native方法来保证原子操作,避免synchronized的高开销,执行效率提升很大。

CAS原理

CAS原理:拿期望值和原本值比较,如果相同则更新成新的值,Unsafe类的objectFieldOffset()方法是一个本地方法,此方法是用来拿到“原来的值”的内存地址,返回值是valueOffset,另外value是一个volative变量,在内存中可见,因此JVM可以抱着个任何时刻任何线程总能拿到该变量的最新值。
AQS(Abstract Queued Synchronized):用来构建锁和同步器的框架,使用AQS能简单高效的构造出应用广泛大量的同步器。
核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态,如果被请求的共享资源被占用,则需要一套线程阻塞等待以及被唤醒时锁分配的机制,此机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系),AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个节点(Node)来实现锁的分配。

AQS对资源的共享方式:

(1)Executive(独占):只有一个线程能执行,如ReentrantLock,又分为公平锁和非公平锁
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去枪锁,谁抢到就是谁的
(2)Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch
AQS底层使用了模板方式模式,同步器的设计是基于模板方法的,自定义同步器一半方式:
(1)使用者继承AbstractQueuedSynchroniser并重写指定的方法;
(2)将AQS组合在自定义同步组件的实现中,并调用i模板方法(调用使用者重写方法)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值