Java IO NIO 并发 锁 详解
Posted wu6660563
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java IO NIO 并发 锁 详解相关的知识,希望对你有一定的参考价值。
文章目录
IO
IO的定义与类型
I/O,即 Input/Output(输入/输出) 的简称。指的是计算机与外部世界或者一个程序与计算机的其余部分的之间的接口。
IO分为几类,Console IO
、Keyboard IO
、File IO
、Network IO
等
Java IO流中分为字节流、字符流
字节流
基于字节的I/O操作接口输入和输出分别是InputStream和OputStream。
InputStream的类层次结构如下:
OutputStream的类层次结构:
字符流
基于字符的读写流
Writer流
Reader流
字节流和字符流转换如下类图:
IO模型
概念上有 5 种模型:blocking I/O(BIO)
,nonblocking I/O(NIO)
,I/O multiplexing (select and poll)
,signal driven I/O (SIGIO)
,asynchronous I/O (the POSIX aio_functions)
。不同的操作系统对上述模型支持不同,UNIX 支持 IO 多路复用。不同系统叫法不同,freebsd 里面叫 kqueue,Linux 叫 epoll。而 Windows2000 的时候就诞生了 IOCP 用以支持 asynchronous I/O。
同步IO和异步IO
从发出IO操作请求开始到IO操作结束的过程中没有任何阻塞,就称为异步,否则为同步
同步IO
同步IO大致分为阻塞IO和非阻塞IO两大类
IO总共有两个阶段:1-等待数据就绪、2-将数据从内核缓冲区复制到用户缓存区
阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其它所有需要这个资源的线程就必须在这个临界区中进行等待,
等待会导致线程挂起。这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其它所有阻塞在这个临界区上的线程都不能工作。非阻塞允许多个线程同时进入临界区
临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。
阻塞IO
blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了
非阻塞IO
NIO 是基于块 (Block) 的,它以块为基本单位处理数据。在 NIO 中,最为重要的两个组件是缓冲 Buffer 和通道 Channel。缓冲是一块连续的内存块,是 NIO 读写数据的中转地。通道标识缓冲数据的源头或者目的地,它用于向缓冲读取或者写入数据,是访问缓冲的接口。Channel 是一个双向通道,即可读,也可写。Stream 是单向的。应用程序不能直接对 Channel 进行读写操作,而必须通过 Buffer 来进行,即 Channel 是通过 Buffer 来读写数据的。
NIO有三大部分:Buffer
、Channel
、Selector
Buffer
Buffer 是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer 对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 Stream 对象中。
在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。
缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
Buffer抽象类有几种子类的基本数据类型的IntBuffer
、FloatBuffer
、CharBuffer
、DoubleBuffer
、ShorBuffer
、LongBuffer
、ByteBuffer
Buffer重要属性
属性 | 字段意思 | 详细描述 |
---|---|---|
capacity | 容量 | 容量指的是缓冲区的大小。容量是在创建缓冲区时指定的,无法在创建后更改。在任何时候缓冲区的数据总数都不可能超过容量。capacity 是指 Buffer 的大小,在 Buffer 建立的时候已经确定。 |
limit | 读写限制 | 读写限制表示的是在缓冲区中进行读写操作时的最大允许位置。比如对于一个容量为32的缓冲区来说,如果设置其limit值为16,那么只有前半个缓冲区在读写时是有用的。如果希望后半个缓冲区也能进行读写操作,就必须把limit设置为32.limit 当 Buffer 处于写模式,指还可以写入多少数据;处于读模式,指还有多少数据可以读。 |
position | 读写位置 | 读写位置表示的是当前进行读写操作时的位置。position 当 Buffer 处于写模式,指下一个写数据的位置;处于读模式,当前将要读取的数据的位置。每读写一个数据,position+1,也就是 limit 和 position 在 Buffer 的读/写时的含义不一样。当调用 Buffer 的 flip 方法,由写模式变为读模式时,limit(读)=position(写),position(读) =0。 |
mark | 标记位置 | 缓冲区支持标记和重置的特征,当调用mark方法时,会在当前的读写位置上设置一个标记。在调用reset方法之后,会使得读写位置回到上一次mark方法设置的位置上。进行标记时的位置不能超过当前的读写位置 。如果通过position方法重新设置了读写位置,而使之设置的标记的位置走出了新的读写位置的范围,那么该标记就会失效。 |
Buffer的方法
clear()
:将position置0,同时将limit设置为capacity的大小,并清除标志mark(置为-1)。clear方法并没有清除缓冲区的内容,只是重置了几个pointer位置。
flip()
:将limit设置到position的位置,然后将position置0,并清除标志mark(置为-1)
rewind()
:positon置0,清除标记位
mark()
:在position处做一下标记
reset()
:position回到上次标记的地方
创建Buffer
ByteBuffer b = ByteBuffer.allocate(15); // 15个字节大小的缓冲区
byte[] buffeer = new byte[2048];
ByteBuffer b1 = ByteBuffer.wrap(buffeer);//包装一个已有的数组来创建
Channel
JAVA NIO中,Channel作为通往具有I/O操作属性的实体的抽象,这里的I/O操作通常指readding/writing,而具有I/O操作属性的实体比如I/O设备、文件、网络套接字等等。光有Channel可不行,我们必须为他增加readding/writing的特性,因此JAVA NIO基于Channel扩展WritableByteChannel和ReadableByteChannel接口。
通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。一个 Buffer 实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。
Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。
正如前面提到的,所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
通道类型通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在 UNIX 模型中,底层操作系统通道是双向的。
1.Opening a FileChannel
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
2.Reading Data from a FileChannel into buffer
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
3.Writing Data to a FileChannel
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining())
channel.write(buf);
4.Closing a FileChannel
channel.close();
Selector
Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。
Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
异步IO
从发出IO操作请求开始到IO操作结束的过程中没有任何阻塞,就称为异步,否则为同步
异步I/O(asynchronous I/O)由POSIX规范定义。演变成当前POSIX规范的各种早起标准所定义的实时函数中存在的差异已经取得一致。一般地说,这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。这种模型与前一节介绍的信号驱动模型的主要区别在于:信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。
并发
并发概念
首先要理解什么是并发,什么是并行?
并发:一个处理器“同时”处理多个任务。这里的同时要打双引号是因为并不是真正意义的同时,是通过时间片切换。
并行:多个处理器(或多核) “同时”处理多个任务
纯CPU密集型的应用
在单核上并发执行多个请求,不能提高吞吐量
由于任务来回场景切换的开销,吞吐量反而会下降
只有多核并行运算,才能有效提高吞吐量
IO密集型的应用
由于请求过程中,很多时间都是外部IO操作,CPU在wait状态,所以并发执行可以有效提高系统吞吐量
线程的使用
-
实现 java.lang.Runnable接口
更加灵活,可以创建一个实现Runnalbe接口的对象,放在多个Thread中执行,这个多个线程共享一个资源
可以创建多个实现Runnable接口的对象,放在多个Thread中执行,这样每个Thread各自拥有一份资源,互不影响 -
继承java.lang.Thread类
每个Thread类只能独享继承Thread的这个类的资源 -
实现java.util.concurrent.Callable接口
Callable是需要返回值的任务。用于要异步获取结果或取消执行任务的场景。
需要借助FutureTask类
public FutureTask(Callable<V> callable)
public boolean cancel(boolean mayInterruptIfRunning)
public V get() throws InterruptedException, ExecutionException
线程的方法
sleep
:睡眠。在sleep期间,不会释放持有的锁
线程结束睡眠后,首先转到就绪状态(Runnable),它不一定会立即运行,而是在可运行池中等待获得cpu。
线程在睡眠时如果被中断,就会收到一个InterrupedException异常。
如:
try
Thread.slee(100);
catch(InterruptedException e)
throw new RuntimeException(e);
yield
:当线程在运行中执行了Thread类的yield()静态方法,如果此时具有相同优先级的其他线程处于就就绪状态。那么yield()方法将把当前运行的线程放到可运行池中并使另一个线程运行,如果没有相同优先级的可运行线程,则yield()方法什么都不做。会让出cpu的控制权,但不会释放锁。
当线程放弃某个稀有的资源(如数据库连接或网络端口)时,它可能调用 yield() 方法临时降低自己的优先级,以便某个其他线程能够运行。
在实际代码中不要使用Thread.yield(),它并不可靠
join
:join方法的功能就是使异步执行的线程变成同步执行
interrupt
:线程中断。
1、中断是一种协作机制,其他线程不能够迫使其他线程停止。
2、中断可以认为是“提醒”。
3、响应中断的阻塞方法可以更容易的取消耗时的操作。
响应线程中断的两种方式:
1、传递InterruptedException
2、恢复中断
wait
:当在一个对象上调用wait方法后,当前线程就会在这个对象上等待。比如,线程A中,调用了obj.wait(),那么线程A就会停止继续执行,而转为等待状态。等待何时结束呢?线程A会一直等到其他线程调用了obj.notify方法为止。此时,obj对象就俨然成了多个线程之间的有效通信手段。
wait和sleep的区别
:wait会释放锁,sleep释放cpu,但不释放锁。
notify
:唤醒。当obj.notify()被调用时,它就会从线程等待队列中,随机选择一个线程,并将其唤醒。
废弃的方法,尽量不用,如:暂停 suspend()
、重新开始 resume()
、销毁 destroy()
、停止 stop()
安全终止线程
使用中断的方式。前提是线程实现的逻辑里,本身响应中断。
1、中断是一种协作机制,其他线程不能够迫使其他线程停止。
2、中断可以认为是“提醒”。
3、响应中断的阻塞方法可以更容易的取消耗时的操作。
Thread.currentThread().interrupt()
使用一个boolean volatile变量来控制是否需要停止任务
public class Shutdown
public static void main(String[] args) throws Exception
Runner one = new Runner();
Thread countThread = new Thread(one, "CountThread");
countThread.start();
// 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束
TimeUnit.SECONDS.sleep(1);
//使用中断的方式
countThread.interrupt();
Runner two = new Runner();
countThread = new Thread(two, "CountThread");
countThread.start();
// 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为false而结束
TimeUnit.SECONDS.sleep(1);
two.cancel();
private static class Runner implements Runnable
private long i;
//要使用volatile
private volatile boolean on = true;
@Override
public void run()
while (on && !Thread.currentThread().isInterrupted())
i++;
System.out.println("Count i = " + i);
public void cancel()
on = false;
线程的状态
新建状态(New)
: 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。就绪状态RUNNABLE
: 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。运行状态(Running)
: 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。阻塞BLOCKED
: 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(01) 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
(02) 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
(03) 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。终止TERMINATED
: 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。等待超时TIMED_WAITING
:超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的。
其他概念
死锁(DeadLock)
:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
饥饿(Starvation)
:饥饿是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。
活锁(LiveLock)
:指事物1可以使用资源,但它让其他事物先使用资源;事物2可以使用资源,但它也让其他事物先使用资源,于是两者一直谦让,都无法使用资源。
一个线程在取得了一个资源时,发现其他线程也想到这个资源,因为没有得到所有的资源,为了避免死锁把自己持有的资源都放弃掉。如果另外一个线程也做了同样的事情,他们需要相同的资源,比如A持有a资源,B持有b资源,放弃了资源以后,A又获得了b资源,B又获得了a资源,如此反复,则发生了活锁。
活锁会比死锁更难发现,因为活锁是一个动态的过程。
吞吐量(throughput)
:吞吐量是指系统在单位时间内处理请求的数量。对于无并发的应用系统而言,吞吐量与响应时间成严格的反比关系,实际上此时吞吐量就是响应时间的倒数。前面已经说过,对于单用户的系统,响应时间(或者系统响应时间和应用延迟时间)可以很好地度量系统的性能,但对于并发系统,通常需要用吞吐量作为性能指标。
CPU密集型 vs IO密集型
CPU密集型
:又叫计算密集型。计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。对于计算密集型任务,最好用C语言编写。
计算密集型,顾名思义就是应用需要非常多的CPU计算资源,在多核CPU时代,我们要让每一个CPU核心都参与计算,将CPU的性能充分利用起来,这样才算是没有浪费服务器配置,如果在非常好的服务器配置上还运行着单线程程序那将是多么重大的浪费。对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换,比较理想方案是:
线程数 = CPU核数+1
也可以设置成CPU核数2,这还是要看JDK的使用版本,以及CPU配置(服务器的CPU有超线程)。对于JDK1.8来说,里面增加了一个并行计算,计算密集型的较理想线程数 = CPU内核线程数2
IO密集型
:IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
对于IO密集型的应用,就很好理解了,我们现在做的开发大部分都是WEB应用,涉及到大量的网络传输,不仅如此,与数据库,与缓存间的交互也涉及到IO,一旦发生IO,线程就会处于等待状态,当IO结束,数据准备好后,线程才会继续执行。因此从这里可以发现,对于IO密集型的应用,我们可以多设置一些线程池中线程的数量,这样就能让在等待IO的这段时间内,线程可以去做其它事,提高并发处理效率。
那么这个线程池的数据量是不是可以随便设置呢?当然不是的,请一定要记得,线程上下文切换是有代价的。目前总结了一套公式,对于IO密集型应用:
线程数 = CPU核心数/(1-阻塞系数)
这个阻塞系数一般为0.8~0.9之间,也可以取0.8或者0.9。套用公式,对于双核CPU来说,它比较理想的线程数就是20,当然这都不是绝对的,需要根据实际情况以及实际业务来调整。
final int poolSize = (int)(cpuCore/(1-0.9))
并发深入
并发优缺点
优点
-
并行程序在多核心cpu有优势,可并行处理任务,减少单个任务的等待时间
比如因为IO操作遇到了阻塞,CPU可以转去执行其他线程,这时并发的优点就显示出来了:更高效的利用CPU,提高程序的响应速度。
Java的线程机制是抢占式的,会为每个线程分配时间片。 -
业务需求。
提供更好的GUI交互体验(如搜狐视频可以边下边播) -
性能需要,为了响应快速。提高服务吞吐量、降低响应时间
-
简化任务调度
-
线程较进程开销更小。
线程一般最小十几K到几十K,而进程需要初始化环境至少需要几十M甚至上百M -
充分利用服务器硬件资源
缺点
-
VM管理内存要求高
对内存管理要求非常高,应用代码稍不注意,就会产生OOM(out of memory),需要应用代码长期和内存泄露做斗争
GC的策略会影响多线程并发能力和系统吞吐量,需要对GC策略和调优有很好的经验 -
设计更复杂
-
线程安全问题
-
调试不方便
-
锁竞争,内存开销
线程安全
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。
怎么保证线程安全呢?
不可变类
如果一个类初始化后,所有属性和类都是final不可变的,则它是线程安全的,不需要任何同步,活性高。
不可变对象永远是线程安全的,因为它的状态在构造完成后就无法再改变了。所以它是线程安全的。String为不可变对象的典型代表。
线程栈内使用
- 方法内局部变量使用
- 线程内参数传递
- ThreadLocal持有
ThreadLocal
顾名思义,它就是thread local variable(线程局部变量)。它的功能非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。
注意: 使用ThreadLocal,一般都是声明在静态变量中,如果不断的创建ThreadLocal而且没有调用其remove方法,将会导致内存泄露。
private static final ThreadLocal<ThreadLocalRandom> localRandom = // ThreadLocal对象都是static的,全局共享
new ThreadLocal<ThreadLocalRandom>() // 初始值
protected ThreadLocalRandom initialValue()
return new ThreadLocalRandom();
;
localRandom.get(); // 拿当前线程对应的对象
localRandom.put(...); // put
防止OOM,记得set以后,需要在特定情况下remove
并发实战
- 无论何种方式,启动一个线程,就要给它一个名字!这对排错诊断系统监控有帮助。否则诊断问题时,无法直观知道某个线程的用途。
Thread thread = new Thread("thread name")
public void run()
// do xxx
;
thread.start();
- 程序应该对线程中断作出恰当的响应。异常不要轻易忽略
- 编写多线程程序,要加相关注释(方法是否线程安全、适用在那种场景使用此类或方法)
- 不要误杀线程(运行定时器时要使全部线程都结束才可以退出)
CAS
全称为Compare-And-Swap,使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。
CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程
Atomic
使用原子操作类,非阻塞,获得最好的性能。开销小,原子性、可见性
AtomicDouble
、AtomicInteger
、AtomicLong
、AtomicBoolean
、AtomicIntegerArray
、AtomicLongArray
、
CAS的问题:
-
ABA问题
因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。 -
自旋时间过长
使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。
volatile
可见性,如果一个变量定义为volatile,在另外一个变量引用,修改了值,会刷新值。这个关键字并不会线程同步
锁
java中最常用的是synchronized 关键字,代表线程安全,给方法或者对象加锁,属于重量级锁
重量级锁synchronized
在JDK1.5之前都是使用synchronized关键字保证同步的,Synchronized的作用相信大家都已经非常熟悉了;
它可以把任意一个非NULL的对象当作锁。
作用于方法时,锁住的是对象的实例(this);
当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。
synchronized(lock)
//code acess shared state
每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁(Intrinsic Lock)或 监视器锁(Monitor Lock).
线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块的时候自动释放锁。
同步块相当于一个互斥锁,最多只有一个线程能持有这种锁。
同步锁是可重入的,一个线程可以获取已持有的锁。
java中的锁,分为乐观锁
、悲观锁
乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
自旋锁
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功。
偏向锁
偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
JVM偏向锁参数:
-XX:+UseBiaseLocking
:启动偏向锁
-XX:BiaseLockingStartupDelay=0
:表示JVM在启动后,立即启用偏向锁。如果不设置该参数,JVM会在启动4s后,才启动偏向锁。
-XX:-UseBiaseLocking
:禁用偏向锁
轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁
重入锁ReentrantLock
ReentrantLock作用和synchiroized关键字相当,但是比synchiroized更加灵活。ReentrantLock本身也是支持重入锁,可以支持对一个资源重复加锁,同时也支持公平锁和非公平锁。所谓的公平锁是指按请求的先后顺序上,先对锁进行请求的就一定先获取锁,即公平锁。反过来就是如果不按时间先后顺序,这种叫做非公平锁。一般来说,非公平锁的效率往往会胜于公平锁,但是在某些特定场景中,可能需要注重时间先后顺序,那么公平锁自然是一个很好的选择。ReentrantLock可以对同一个线程加锁多次,但是加锁多少次,就必须解锁多少次。才能成功释放锁。
属于独占锁
提供了与synchronized相同的功能
比synchronized提供了更多的灵活性(不能中断那些正在等待获取锁的线程、并且在请求锁失败的情况下,必须无限期等待)
ReentrantLock可以创建公平和非公平的锁,内部锁不能够选择,默认是非公平锁
当需要可定时的、可轮询的、可识别中断、或者公平锁的时候,使用ReentrantLocak,否则使用内部锁
import java.util.concurrent.locks.ReentrantLock;
public class ReenterLock implement Runnable
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
public void run()
for(int j = 0;j < 10000; j++)
lock.lock();
//支持重入锁
lock.lock();
try
i++;
finally
lock.unlock();
lock.unlock();
public static void main(String[] args) throws InterruptedException
ReenterLock tl = new ReenterLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
t1.join();
t2.join();
//此时两个线程join以后,因为对线程加锁了,所以结果是20000
System.out.println(i);
//查询当前线程锁的次数
int getHoldCount();
Java NIO全面详解(看这篇就够了)