- Java并发—基础
- 创建线程的两种方法
- 中断线程
- 线程池
- service.awaitTermination:
- 线程池的原理及作用
- 线程池的优点
- 线程池的核心组件
- 线程的生命周期
- 线程池的拒绝策略
- JDK提供常用的线程池:
- 使用newFixedThreadPool举例
- 使用newWorkStealingPool()举例
- 使用newScheduledThreadPool()举例
- schedule和scheduleAtFixedRate:
- 线程池工作流程:
- 给线程设定优先级
- yield
- join
- 守护线程:
- 线程之间的通信:
- sleep和wait的区别*****
- Java并发--锁
- 乐观锁和悲观锁
- 乐观锁
- 悲观锁
- 自旋锁
- 自旋锁的优缺点
- 自旋锁的时间阈值
- synchronized
- 死锁
- 线程同步
- 各种锁
- ReentrantLock
- ReentrantLock是如何避免死锁的
- 公平锁与非公平锁
- tryLock,lock和lockInterruptibly()的区别
- synchronized和ReentrantLock区别*****
- Semaphore
说到并发,我们就不得不提进程和线程的关系。进程和线程的关系和区别下面是我在知乎上看到的一个很生动的比喻:
- 开个QQ,开了一个进程;开了迅雷,开了一个进程。在QQ的这个进程里,传输文字开一个线程、传输语音开了一个线程、弹出对话框又开了一个线程。所以运行某个软件,相当于开了一个进程。在这个软件运行的过程里(在这个进程里),多个工作支撑的完成QQ的运行,那么这“多个工作”分别有一个线程。所以一个进程管着多个线程。通俗的讲:“进程是爹妈,管着众多的线程儿子”
由于进程是由操作系统统一分配资源,所以本文主要以介绍线程为主。
创建线程的两种方法- Thread类和Runnable接口(Thread类最终也是实现Runnable接口),每个线程都通过执行Runnable对象中的run()方法来开始他的生命周期。其中run方法必须是公有的不带任何参数,没有返回值,且不抛出异常。
- 创建线程后新的线程都将保持在空闲状态,知道调用他的start的方法来唤醒并开始执行目标对象的run方法。在一个线程的生命周期中只能调用一次start方法一旦线程启动之后,将一直保持运行状态,直至目标对象的run方法返回才技术。
使用继承Thread类的方法进行创建线程:
Thread类k看似代码简介其实最终也是实现Runnable接口。而且Thread是一个,调用者只能继承一次。
public class A{
public static void main(String[] args) {
A0 a0=new A0();
A0 a1=new A0();
a0.start();
a1.start();
}
}
class A0 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
if(i==50){
try {
Thread.sleep(5000);//线程暂停5000ms后被重新唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
使用实现Runnable接口进行创建线程:
public class B {
public static void main(String[] args) {
B0 b0=new B0();
Thread t0=new Thread(b0);
t0.start();
B0 b1=new B0();
Thread t1=new Thread(b1);
t1.start();
}
}
class B0 implements Runnable {
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
中断线程
- start方法有一个可以永久结束线程的方法–stop(),但是在非特殊情况下不会使用该方法,因为他是一种类似于机动车在高速公路上高速行驶时突然急踩刹车一样,对机动车造成伤害。同样stop方法也会对代码逻辑造成伤害。
- 所以jdk更推荐使用interrupt来中断线程,同样使用interrupt也是有限制的,当线程处在sleep,wait,join状态的时候才被允许用interrupt。interrupt的作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。笔者给大家推荐一个关于详细介绍interrupt的文章。
经过前面的介绍,相信大家对线程的创建和中断已经不陌生了,但是普通创建线程的方法有一个弊端就是不能进行返回,那么接下来我将会引入一种更加方便的创建线程的方法–线程池。
使用Callable和线程池实现有返回值的线程:
- 有时我们需在主线程中开启多个线程并发执行一个任务,然后收集各个线程执行返回的结果并最终汇总起来,这个时候就需要Callable接口。操作过程如下
1. 创建一个类并实现Callable接口,在call方法中实现具体的运算逻辑并返回结果
2. 创建一个线程池,一个用于接收返回结果的Future List及Callable的线程实例,使用线程池提交任务并将程序执行执行之后的结果保存在Future中,在线程执行后遍历FutureList中的Future对象,在该对象上调用get方法就可以获取Callable线程任务返回的数据并汇总结果。(大家对future或许比较陌生,如果想更加深入了解Future的请大家阅读文章一,文章二)
public class C {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService service=Executors.newCachedThreadPool();
List list=new ArrayList();
for(int i=0;i<10;i++){
C0 c=new C0(i+" ");
Future f=service.submit(c);
list.add(f);
}
service.shutdown();
for(Future f:list){
System.out.println(f.get().toString());
}
}
}
class C0 implements Callable{
String name;
C0(String name){
this.name=name;
}//通过构造器把局部变量变成全局变量
@Override
public Object call() throws Exception {
return this.name+"is ok";
}
}
service.awaitTermination:
public class D {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
service.execute(new Runnable() {//多线程,因为他将循环放在了外面,多次执行 Runnable()对应execute ,Callable对应submit
@Override
public void run() {
System.out.println(Thread.currentThread().getName() );
}
});
}
service.shutdown();
// shutdown方法:平滑的关闭ExecutorService,当此方法被调用时,
// ExecutorService停止接收新的任务并且等待已经提交的任务(包含提交正在执行和提交未执行)执行完成。
// 当所有提交任务执行完毕,线程池即被关闭。
try {
service.awaitTermination(10, TimeUnit.SECONDS);//await等待 Termination终止
// awaitTermination方法:接收人timeout和TimeUnit两个参数,用于设定超时时间及单位。
// 当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。
// 一般情况下会和shutdown方法组合使用。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程池的原理及作用
Java中的线程池主要用于管理线程组及其运行状态,以便Java虛拟机更好地利用CPU资源。
- Java线程池的的工作原理为******:
JVM先根据用户参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会返回现有可用的线程,进而再次从队列中取出任务并执行。 - 线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大并发数、以保证高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行。使用线程池启动线程:处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。
-
降低资源消耗。减少了创建和销毁次数,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
-
提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
- 线程池管理器:用于创建并管理线程
- 工作线程:线程池中执行具体任务的线程
- 任务接口:用于定义工作线程的调用和执行策略,只有线程执行了该接口,线程中的任务才能被线程池调度
- 任务队列:存储待处理的任务,新的任务会不断增加到队列中去,执行完成的任务将被从队列中移除
笔者看到过一篇更好文章来介绍线程池,请点击点击此处。
线程池的拒绝策略线程池中核心线程数被用完而且阻塞队列已经被排满,此时线程池中资源已经耗尽,线程池中没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池中将采用拒绝策略处理新添加的线程任务。
- AbortPolicy:直接抛出异常,阻止线程正常运行
- CallerRunsPolicy:如果被丢弃的任务未关闭,则执行该线程任务
- DiscardOrdersPolicy:移除线程队列中最早的一个线程任务并尝试提交当前任务。
- DiscardPolicy:丢弃当前线程,不做任何处理。目前来看是最好的一种方法。
ThreadPoolExecutor:
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
ForkJoinPool:
- newWorkStealingPool :内部使用多个队列来减少各个线程调度产生的竞争,工作窃取指的是闲置的线程去处理本不属于他的任务。每个处理器核都有一个队列存储着需要完成的任务,对于多核处理机来说,当一个核对应的任务处理完毕后。就可以去帮助其他核处理任务。
public class F {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
for(int i=0;i<2;i++){
service.execute(
new Runnable() {
@Override
public void run() {
for(int i=0;i<20;i++){
System.out.println(Thread.currentThread().getName()+" :"+i);
}
}
}
);
}
}
}
使用newWorkStealingPool()举例
// 假设共有三个线程同时执行, A, B, C
// 当A,B线程池尚未处理任务结束,而C已经处理完毕,则C线程会从A或者B中窃取任务执行,
// 这就叫工作窃取
// 假如A线程中的队列里面分配了5个任务,而B线程的队列中分配了1个任务,当B线程执行完任务后,它会主动的去A线程中窃取其他的任务进行执行
// WorkStealingPool 背后是使用 ForkJoinPool实现的
public class G {
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors());
ExecutorService service= Executors.newWorkStealingPool();
service.execute(new R(1000));// 我的cpu核数为12 启动13个线程,
//第一个是1s执行完毕,其余都是2s执行完毕,
// 有一个任务会进行等待,当第一个执行完毕后,会再次偷取第十三个任务执行
//看线程 有重复的线程去执行
for(int i=0;i
输出结果;
使用newScheduledThreadPool()举例
public class E {
public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService service = Executors.newScheduledThreadPool(3);
//第一种
// service.schedule(
// new Runnable() {
// @Override
// public void run() {
// System.out.println(Thread.currentThread().getName());
// }
// },3, TimeUnit.SECONDS);
// service.shutdown();
// service.awaitTermination(10,TimeUnit.SECONDS);
//第二种
service.scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
},1, 3,TimeUnit.SECONDS);延迟一秒开始,但是每隔3秒 线程执行
}
}
schedule和scheduleAtFixedRate:
schedule和scheduleAtFixedRate的区别在于,如果指定开始执行的时间在当前系统运行时间之前,scheduleAtFixedRate会把已经过去的时间也作为周期执行,而schedule不会把过去的时间算上。
SimpleDateFormat fTime = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date d1 = fTime.parse("2005/12/30 14:10:00");
t.scheduleAtFixedRate(new TimerTask(){
public void run()
{
System.out.println("this is task you do6");
}
},d1,3*60*1000);
间隔时间是3分钟,指定开始时间是2005/12/30 14:10:00,如果我在14:17:00分执行这个程序,那么会立刻打印3次
this is task you do6 //14:10
this is task you do6 //14:13
this is task you do6 //14:16
并且注意,下一次执行是在14:19 而不是 14:20。就是说是从指定的开始时间开始计时,而不是从执行时间开始计时。
但是上面如果用schedule方法,间隔时间是3分钟,指定开始时间是2005/12/30 14:10:00,那么在14:17:00分执行这个程序,则立即执行程序一次。并且下一次的执行时间是 14:20,而不是从14:10开始算的周期(14:19)。
线程池工作流程:
给线程设定优先级
优先 线程中有10个优先级,数字越大证明优先级越高。MAX_PRIORITY为最大的优先级。
public class H {
public static void main(String[] args) {
H0 h0=new H0();
h0.setName("A");
h0.setPriority(Thread.MAX_PRIORITY);//Priority : 优先 线程中有10个优先级,数字越大证明优先级越高
H0 h1=new H0();
h1.setName("B");
h1.setPriority(Thread.MIN_PRIORITY);
h0.start();
h1.start();
}
}
class H0 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
输出结果:
线程A先于线程B执行的概率要大,
但不是全部的A线程都会在线程B前面。
只能说线程A在前面的多
yield
用sleep的方式使效果更加明显。不用sleep也会有礼让的趋势
public class H {
public static void main(String[] args) {
H0 h0=new H0();
h0.setName("A");
H0 h1=new H0();
h1.setName("B");
h0.start();
h1.start();
}
}
class H0 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
yield();//yield的意思是“屈服、礼让”,在程序中表现为当前线程会尽量让出CPU资源来给其他线程执行
try {
Thread.sleep(100);//进程运行的太快了,用sleep使线程慢下来
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
输出结果:
join
让一个线程去等待另一个线程,直到另一个线程全部执行完成后,才可以执行该线程
public class I{
public static void main(String[] args) {
I0 i0=new I0();
i0.setName("A");
for(int i=0;i<100;i++){
if(i==50){
i0.start();
try {
i0.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
class I0 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
输出结果:将主线程调为10,A线程调为1
守护线程:
setDaemon()方法标记一个线程是守护线程
线程分为用户线程和守护线程。
他们的区别在于
- 用户线程:Java虚拟机在所有的用户线程都离开后Java虚拟机才离开。
- 守护线程:他依赖于JVM,于JVM共生死,在JVM中所有的线程都是守护线程时,JVM就可以退出了,如果有一个非守护线程,JVM都不会退出。
守护线程优先级比较低,用于为系统中其他的对象和线程提供服务,比如垃圾回收器是一个经典的守护线程,如果在我们的程序中不再有任何线程运行时,程序就不会产生垃圾,垃圾回收器也就无事可做。垃圾回收器会消灭。守护线程始终在低状态下运行,用于实时监控和管理系统中的可回收资源。
public class J {
public static void main(String[] arrgs) {
J1 j1=new J1();
j1.start();
J2 j2=new J2();
j2.setDaemon(true);//定义为守护线程
j2.start();
}
}
class J1 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"用户:"+i);
}
}
}
class J2 extends Thread{
@Override
public void run() {
for(int i=0;i<1000;i++){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"守护:"+i);
}
}
}
输出结果:(一旦用户线程停止,守护线程也会停止)
线程之间的通信:
一、为什么要线程通信?
-
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
-
当然如果我们没有使用线程通信来使用多线程共同操作同一份数据的话,虽然可以实现,但是在很大程度会造成多线程之间对同一共享变量的争夺,那样的话势必为造成很多错误和损失!
-
所以,我们才引出了线程之间的通信,多线程之间的通信能够避免对同一共享变量的争夺。
二、什么是线程通信?
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。
就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。
于是我们引出了等待唤醒机制:(wait()、notify())
就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify);
(1)wait()方法:
线程调用wait()方法,释放它对锁的拥有权,同时他会在等待的位置加一个标志,为了以后使用notify()或者notifyAll()方法 唤醒它时,它好能从当前位置获得锁的拥有权,变成就绪状态,
要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或synchronized块中。 在哪里等待被唤醒时,就在那里开始执行。
(2)notify/notifyAll()方法:
- notif()方法:notify()方法会唤醒一个等待当前对象的锁的线程。唤醒在此对象监视器上等待的单个线程。
- notifAll()方法:notifyAll()方法会唤醒在此对象监视器上等待的所有线程。
下面我将写一个demo来进行演示
我的demo的大致意思是,银行里在进行存取款的时候。只有存钱后,才能取钱。通过线程同步来实现线程之间的通信
- 账户类:
public class Account {
private String no;
private int balance;//本金
private boolean flag;//旗标 false:没有存款 可以存了 true:有存款 可以取了
public String getNo() {
return no;
}
public void setNo(String no) {
this.no = no;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public synchronized void saveMoney(int money){//存钱线程调用
if(flag){//有存款
try {
wait();//存钱线程等候
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else{//!flag没有存款,可以继续存钱
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance+=money;
System.out.println(this.no+"存了"+money);
flag=true;
notifyAll();//唤醒所有进程
}
}
public synchronized void getMoney(int money){//取钱线程调用
if(!flag){//无存款
try {
wait();//存钱线程等候
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else{//flag有存款,可以取钱
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance-=money;
System.out.println(this.no+"取出"+money);
flag=false;
notifyAll();//唤醒所有进程
}
}
}
- 取钱类
public class Get extends Thread {
private Account account;
private int money;
public Get(Account account, int money){
this.account=account;
this.money=money;
}
@Override
public void run() {
this.account.getMoney(money);
}
}
- 存钱类
public class Save extends Thread {
private Account account;
private int money;
public Save(Account account,int money){
this.account=account;
this.money=money;
}
@Override
public void run() {
this.account.saveMoney(money);
}
}
- 测试类
public class Test {
public static void main(String[] args) {
Account account =new Account();
account.setBalance(100);
account.setNo("狗");
for(int i=0;i<10;i++){
Save save=new Save(account,100);
Get get=new Get(account,100);
save.start();
get.start();
}
}
}
输出结果:(注意将代码中的10换成了3)
这个demo很好理解,笔者主要介绍synchronized这个关键字,笔者向大家推荐一篇关于synchronized的文章。
sleep和wait的区别*****
- sleep方法属于Thread类,wait属于Object类
- sleep方法暂时让该线程让出CPU给其他线程2,但其监控状态依然保持,在指定的时间过后会自动恢复运行状态。
- 在调用sleep方法时,线程不会释放对象锁。
- 在调用wait方法时,线程会释放对象锁,进入等待此对象的等待锁池,只有针对此对象调用notify()方法后,该线程才能进入对象锁池准备获取对象锁,并进入运行状态。
Java并发–锁
Java中的锁主要用于保障并发线程情况下数据的一致性。
- 锁从乐观和悲观角度分别为乐观锁和悲观锁
- 从获取资源公平角度来看又分为公平锁和非公平锁
- 从是否共享资源角度来看又分为共享锁和独占锁
- 从锁的状态又分为偏向锁,轻量级锁和重量级锁
- 同时,在JVM中还使用了自旋锁以更快的使用CPU资源
乐观锁和悲观锁
笔者看多过一个认为很不错的关于乐观锁和悲观锁的文章。
乐观锁
乐观锁(Optimistic Lock): 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作),乐观锁适用于多读的应用类型,这样可以提高吞吐量
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本(version)或者是时间戳来实现,不过使用版本记录是最常用的。
Java中乐观锁大部分是通过CAS(Compare And Swap 比较和交换)操作来实现的。
悲观锁
悲观锁(Pessimistic Lock): 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放,悲观锁中的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
但是在效率方面,处理加锁的机制会产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,如果已经锁定了一个线程A,其他线程就必须等待该线程A处理完才可以处理
Java中乐观锁大部分是通过AQS( AbstractQueuedSynchronizer 抽象的队列同步器)操作来实现的。
数据库中的行锁,表锁,读锁(共享锁),写锁(排他锁),以及syncronized实现的锁均为悲观锁
自旋锁
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态到用户态之间的切换进入阻塞,挂起的状态只需在内核态等一等(也叫做自旋),在等待持有锁的线程释放锁后即可立即获得锁,这样就避免了用户在线程在内核状态的切换上导致的锁时间消耗。
线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时,将会产生CPU的浪费,有时候还会发生线程永远不会获取锁,而导致CPU别用就占用,所以就需要设定一个自选等待的最大时间。在线程执行的时间超过自选等待的最大时间后,线程会退出自旋模式,并释放持有的锁。
自旋锁的优缺点
- 优点:它可以减少CPU上下文切换,对于占用锁的时间非常短,或者在锁竞争不激烈的代码来说性能大幅度提高,因为自旋的CPU耗时明显少于线程阻塞挂起再唤醒时两次CPU上下文切换所用的时间。
- 缺点:在持有锁的线程占用锁的时间过长或者锁的竞争过于激烈时,线程的自选过程中会长时间获取不到锁资源,将引起CPU的浪费。所以在系统中有复杂锁依赖情况下不适合采用自旋锁。
自旋锁的时间阈值
JDK1.5是固定的自选时间,JDK1.6引入了适应性自旋锁。他的时间不再是固定的,而是由上一次在同一个锁上的自旋时间和锁的拥有者的状态来决定的,可基本认为一个上下文切换的时间就是一个最佳的时间。
synchronized
synchronized用于为Java对象,方法,代码块提供线程安全的操作。synchronized属于独占式的悲观锁,同时属于可重入锁。
在使用synchronized装饰对象时,同一时刻只能有一个线程对该对象进行访问;
在使用synchronized修饰方法,代码块时,同一时刻只能有一个线程对该方法体或代码块进行访问,其他线程只有等待当前线程执行完成并释放资源后才能访问该对象或者执行同步代码块。
Java中每个对象都有个monitor对象,加锁就是在竞争monitor对象对代码加锁是通过在前后加上monitorenter和monitorexit指令来实现的,对方法加锁是通过一个标记来判断的。
synchronized适用范围:
- synchronized作用于成员变量和非静态方法时,锁住的是对象实例,即this对象。
- synchronized作用于静态方法时,锁住的对象是Class实例,因为静态方法属于Class而不属于对象。
- synchronized作用于一个代码块时锁住的的是所有代码块中配置的对象
- 当把方法设为静态时,syn就相当于全局锁,会锁柱所有调用该方法的线程,因此就算是多个对象也不会争抢。
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
总结:
1、 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是类,该类所有的对象同一把锁。
2、每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
3、实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制
//synchronized
public class A {
public static void main(String[] args) {
A0 a0 = new A0();
A0 a1 = new A0();
new Thread(new Runnable() {
@Override
public void run() {
a0.ok1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
a1.ok2();
}
}).start();
}
}
class A0{
void ok1(){//synchronized 锁止同一个对象
synchronized(A.class){
try {
for (int i = 1; i < 30; i++) {
System.out.println("OK1执行" + i + "次!");
Thread.sleep(300);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized void ok2(){
synchronized(A.class){
try {
for (int i = 1; i < 30; i++) {
System.out.println("OK2执行" + i + "次!");
Thread.sleep(300);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出结果:(注意代码中的30换成了3)
死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,如果没有外部干涉,他们将都无法推进下去。此时称系统处于死锁状态。
//死锁
public class B {
public static void main(String[] args) {
B0 a0=new B0();
new Thread(new Runnable(){
@Override
public void run() {
a0.ok();
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
a0.ok1();
}
}).start();
}
}
class B0{
private String lockName1="A";
private String lockName2="B";
void ok(){//synchronized 锁止同一个对象
synchronized (lockName2){
try {
for (int i = 1; i < 30; i++) {
System.out.println("OK执行" + i + "次!");
Thread.sleep(300);
synchronized (lockName1){}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
void ok1(){
synchronized (lockName1) {
try {
for (int i = 1; i < 30; i++) {
System.out.println("OK1执行" + i + "次!");
Thread.sleep(300);
synchronized (lockName2) {
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程同步
Account类:
public class Account {
private double balance;
public Account(){}
public void setBalance(double balance)
{
this.balance = balance;
}
public double getBalance()
{
return this.balance;
}
}
getMoney类:
一个取钱系统,存入一定量的钱,分多次取出。
public class getMoney extends Thread {
private String name;
private double money ;
private Account account;
public getMoney(String name,double money,Account account){
this.name=name;
this.money=money;
this.account=account;
}
@Override
public void run() {
synchronized (this.account){
if(this.account.getBalance()>=this.money){
System.out.println("吐出"+this.money+"钱");
try {
Thread.sleep(100);
this.account.setBalance(this.account.getBalance()-this.money);
System.out.println("余额剩余"+this.account.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
Test类:
public class Test {
public static void main(String[] args) {
Account a=new Account();
a.setBalance(500);
for(int i=0;i<5;i++){
getMoney getmoney=new getMoney("甲",100,a);
getmoney.start();
}
}
}
运行结果:
各种锁
ReentrantLock
- ReentrantLock继承了lock接口,是可重入的互斥锁,虽然具有与synchronized相同功能,但是会比synchronized更加灵活(具有更多的方法)。
- ReentrantLock通过AQS来实现锁的获取与释放
- 独占锁指该锁在同一时刻只能被一个线程获取,而获取锁的其他线程只能在同步队列中等待;
- 可重入锁是指该锁能够支持一个线程对同一个资源执行多次加锁操作。
- ReentrantLock支持公平锁和非公平锁的的实现。
- ReentrantLock之所以被称为可重入锁,是因为它可以反复进入。即允许连续两次获得同一把锁,两次释放同一把锁,但如果不对称会抛出异常。
public class C implements Runnable {
ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
C c = new C();
Thread t = new Thread(c);
Thread t1 = new Thread(c);
t1.start();
t.start();
}
@Override
public void run() {
try {
lock.lock();
int i = 0;
for (int j = 0; j < 100; j++) {
i++;
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + ":" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
}
运行结果:
ReentrantLock是如何避免死锁的
-
响应中断:如果有一个线程尝试获取一把锁,有两种情况,一种是获取锁继续执行,另一种是继续等待,而 ReentrantLock还提供了可响应中断的可能,即在等待所的情况下 ,线程可以根据需求取消对锁的请求
-
可轮询锁
-
定时锁
public class D {
ReentrantLock lock = new ReentrantLock();
public void doWork(){
String name=Thread.currentThread().getName();
try{
//响应中断
// if(name.equals("B")){
//
// Thread.currentThread().interrupt();
// }
//可轮询锁
// System.out.println(name+":"+lock.tryLock());
// if(!lock.tryLock()){//获得不到 立刻返回
// return;
// }
// lock.lock();
// lock.lockInterruptibly();//可以通过interrupt方法进行中断锁 不需要通过unlock释放锁,通过interrupt进行自动释放
//定时锁
// if(!lock.tryLock(10, TimeUnit.SECONDS)){//如果在10秒钟内获得了锁,则继续工作
// return;
// }
System.out.println(name+"得到锁");
System.out.println(name+"执行");
for(int i=0;i<2;i++){
Thread.sleep(1000);
System.out.println(name+i+"ok");
}
}catch(Exception e){
System.out.println(name + " 被中断");
System.out.println("因此"+name + "线程可以做些别的事情");
}finally{
if(lock.isHeldByCurrentThread()){//如果锁住了当前线程 释放锁
lock.unlock();
}
System.out.println(name + " 正常释放锁");
}
}
public static void main(String[] args) {
D d=new D();
Thread t1= new Thread(new Runnable() {
@Override
public void run() {
d.doWork();
}
});
Thread t2= new Thread(new Runnable() {
@Override
public void run() {
d.doWork();
}
});
t1.setName("A");
t2.setName("B");
t1.start();
t2.start();
}
}
输出结果
响应中断:
可轮询锁:
定时锁:
公平锁与非公平锁
如果大家对ReentrantLock公平锁与非公平锁源码有兴趣可以可以参考下面文章。
深入剖析ReentrantLock公平锁与非公平锁源码实现
ReentrantLock默认支持非公平锁。
-
公平锁指的是分配和竞争机制是公平的,既遵循先到先得原则。
行文至此,我相信你对 公平锁与非公平锁 已经多少不是那么陌生了。笔者写到这一定要感谢一个应该是人的人,因为就在刚刚我已经提醒他老实的否则我就会让他永远存在我的blog里,他不但不听还威胁我。笔者幼小的心灵受到了严重的打击。那个人就是。那就是来自佳木斯大学万千少男的梦——迪古拉斯–迪姐的支持。如果笔者没有想嘲笑她的强烈愿望,也没可能完成的这么快的完成。行文最后,把最好的祝福送给我迪姐。祝我迪姐期末考试有一科分数是59。吃方便面没有调料包。上厕所房顶滴水。世间"没好"与她环环相扣。但是,注意此处有转折,肯定会考到理想的研究生。哈哈哈哈哈哈哈
-
非公平锁:JVM遵循随机,就近原则分配锁的机制,执行效率明显高于公平锁。
tryLock,lock和lockInterruptibly()的区别
- tryLock:若有可用锁,则获取该锁并返回true,否则返回false,不会有延迟或等待; tryLock还 可以增加时间限制,如果超过了指定的时间还没获得锁,则返回false。
- lock:若有可用锁,则获取该锁并返回true,否则会一-直等待直到获取可用锁。
- lockInterruptibly:在锁中断是会抛出异常,lock不会。
synchronized和ReentrantLock区别*****
- 共同点如下:
- 都用于控制多线程对共享对象的访问。
- 都是可重入锁。
- 都保证了可见性和互斥行。
- 不同点如下:
- R显示获取和释放锁,必须在finally控制块中进行解锁;
s隐式获取和释放锁。 - R可响应中断,可轮回,为处理锁提供了更多的灵活性。
- R是API级别的,s是JVM级别的。
- R可以定义公平锁。
- 二者底层实现不同,s是同步阻塞,采用的是悲观并发策略; Lock是同步非阻塞,采用的是乐观并发策略。
- Lock是一个接口,而s是Java中的关键字,s是由内置的语言实现的。
- 我们通过Lock可以知道有没有成功获取锁,通过s却无法知道。
- Lock可以通过分别定义读写锁提高多个线程读操作的效率。
Semaphore
它是一种基于计数的信号量,在定义信号量对象时可以设定一个阈值,基于该阈值,多个线程竞争获取许可信号,线程竞争到许可信号后开始执行具体业务逻辑,业务逻辑在执行完成后释放该许可信号。在许可信号的竞争队列超过阈值后,新加入的申请许可信号的线程将被阻塞,直到有其他许可信号被释放。
Semaphore对锁的申请和释放同ReentrantLock类似,通过acquire和release进行获取和释放资源。其中 acquire和R中的lockInterruotibly方法一样,是一种可响应的中断锁,也就是说在等待许可信号资源的过程中可以被Thread. interrupt方法中断而取消对许可信号的申请。
S也实现了可轮询的锁请求,定时锁的功能,以及公平锁与非公平锁的机制。S的释放操作也需要手动执行,因此,为了避免线程因执行异常而无法正常释放锁,释放锁的操作必须在finally代码块中完成。
//Semphore 通过开取信号量的个数 和申请信号量的个数 来调整是否同时干 还是抢着干
// 开取的个数多,申请的少,抢着干
// 开的个数和申请的个数形同,分别干 !
public class E {
private Semaphore semaphore=new Semaphore(3);//同时开取三个信号量
public void doWork(){
String name=Thread.currentThread().getName();
try {
semaphore.acquire(1);//申请多少信号量。不写个数默认为一。如果开一个信号量,而且有多个线程,那么多个线程争抢一个信号量
System.out.println(name + " 得到锁");
System.out.println(name + " 开工干活");
for(int i=0;i<100;i++){
System.out.println(name +"干活" + i);
}
}catch(Exception e){
System.out.println(name + " 被中断");
System.out.println("因此"+name + "线程可以做些别的事情");
}finally{
semaphore.release(2);
System.out.println(name + " 正常释放锁");
}
}
public static void main(String[] args) {
E d=new E();
Thread t1=new Thread(new Runnable(){
@Override
public void run() {
d.doWork();
}
});
Thread t2=new Thread(new Runnable(){
@Override
public void run() {
d.doWork();
}
});
Thread t3=new Thread(new Runnable(){
@Override
public void run() {
d.doWork();
}
});
Thread t4=new Thread(new Runnable(){
@Override
public void run() {
d.doWork();
}
});
t3.setName("A");
t2.setName("B");
t1.setName("C");
t4.setName("D");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
输出结果:
代码中是工作100次,输出结果是工作一次。



