java多线程入门篇 (三)
- 锁与同步
- 等待/通知机制
- 信号量:
- 管道:
- 其它通信相关
- join方法
- sleep方法
- ThreadLocal类
- InheritableThreadLocal
java多线程入门篇(一)进程线程基本概念和入门类与接口
Java多线程入门篇(二)线程组线程优先级和线程状态
在Java中合理的使用多线程可以更好的利用服务器资源。一般来说,线程都有自己的自由线程上下文,互不干扰。但是当我们需要多个线程之间互相协作的时候,就需要掌握Java线程的通信方式才可以
锁与同步
在Java中锁都是基于对象的,所以又称为对象锁。锁与线程的关系就像婚姻关系,一个锁在同一时间只能由一个线程持有,只有当持有锁的线程释放锁后其它线程才能获得锁。
同步就是让多个线程按照一定的顺序执行。而锁就是用于实现这个功能。
接下来我们看一个不加锁的程序:
public class Synchroniz {
static class ThreadA implements Runnable{
@Override
public void run(){
for (int i = 0;i<100;i++){
System.out.println("线程A"+i);
}
}
}
static class ThreadB implements Runnable{
@Override
public void run(){
for (int i = 0;i<100;i++){
System.out.println("线程B"+i);
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
new Thread(new ThreadB()).start();
}
}
一部分结果:
线程A9
线程A10
线程B0
线程A11
线程B1
线程A12
线程B2
线程A13
从上面可以看出两个线程是各自独立工作的,输出自己的打印值。如果重新运行每次的结果都会不一样。
接下来一个新的需求:希望A先执行完再执行B,那应该怎么办呢?最简单的方式就是使用一个“对象锁”。
实例代码:
public class Synchroniz1 {
private static Object lock = new Object();
static class ThreadA implements Runnable{
@Override
public void run(){
synchronized (lock){
for (int i = 0;i<100;i++){
System.out.println("线程A"+i);
}
}
}
}
static class ThreadB implements Runnable{
@Override
public void run(){
synchronized (lock){
for (int i =0;i<100;i++){
System.out.println("线程B"+i);
}
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new ThreadB()).start();
}
}
部分结果:
线程A96
线程A97
线程A98
线程A99
线程B0
线程B1
线程B2
这里声明了一个名字为lock的对象锁。我们在ThreadA和ThreadB中需要同步的代码块里,都是用synchronized关键字加上了同一个对象锁lock。这样同一时间就只有一个线程可以持有一个锁。
等待/通知机制
上面一种基于”锁“的方式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会消耗服务器资源,而等待/通知机制是另一种方式。
Java的等待/通知机制是基于Object类的wait()方法和notify(),notifyAll()方法实现的。(notify()方法随机叫醒一个正在等待的线程,notifyAll()叫醒所有正在等待的线程。)
我们知道一个锁同一时刻只能被一个线程持有,如果线程A持有了锁lock并开始执行,它可以调用lock.wait()让自己进入等待状态,此时lock锁是被释放了的。
然后线程B获得了锁并执行,然后调用notify()唤醒之前持有锁的线程。
注意:唤醒不代表运行,不代表获得锁,还是需要等当前持有锁的线程释放锁之后,其它线程才可以去竞争获得锁。
代码实现:
public class Synchroniz2 {
private static Object lock = new Object();
static class ThreadA implements Runnable{
@Override
public void run(){
synchronized (lock){
for (int i = 0;i<5;i++){
System.out.println("ThreadA"+i);
try {
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
static class ThreadB implements Runnable{
@Override
public void run(){
synchronized (lock){
for (int i = 0;i<5;i++){
System.out.println("ThreadB"+i);
try {
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
ThreadA0
ThreadB0
ThreadA1
ThreadB1
ThreadA2
ThreadB2
ThreadA3
ThreadB3
…
在上面的例子中,线程A,B都是先打印出自己需要打印的东西,然后唤醒另一个线程,自己再进入等待并释放锁。所以呈现的就是A,B线程交叉打印。
注意:等待/通知机制需要线程使用同一个对象锁,如果两个线程使用的是不同的对象锁,那他们是不可以使用等待/通知机制的。
信号量:
信号量 (Semaphore),有时被称为信号灯,是在 多线程 环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被 并发 调用。 在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。
jdk提供了类似与”信号量“功能的类:Semaphore。不过接下来我要介绍的是基于volatile关键字自己实现信号量通信的方式。
volatile关键字能够保证内存可见性,如果用volatile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其他线程是立马可见更改之后的值的。
需求:我希望线程A输出0,线程B输出1,线程A输出2,线程B输出3,一次类推,改如何做?
实例代码:
public class SemaphoreDemo {
private static volatile int signal = 0;
static class ThreadA implements Runnable{
@Override
public void run(){
while (signal<5){
if (signal%2==0){
System.out.println("ThreadA"+signal);
synchronized (this){
signal++;
}
}
}
}
}
static class ThreadB implements Runnable{
@Override
public void run(){
while (signal<5){
if (signal%2==1){
System.out.println("ThreadB"+signal);
synchronized (this){
signal++;
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
new Thread(new ThreadB()).start();
}
}
ThreadA0
ThreadB1
ThreadA2
ThreadB3
ThreadA4
ThreadB5
通过上面的代码我们使用volatile关键字实现了信号量模型。
注意:volatile变量需要进行原子操作,signal++并不是原子操作所以我们需要使用synchronized给他上锁。
管道:
管道是基于”管道流“的通信方式,jdk提供了PipedWriter,PipedReader,PipedOutputStream,PipedInputStream。其中,前面两个是基于字符的,后面两个是基于字符流的。
实例代码:
public class pipe {
static class ReaderThread implements Runnable{
private PipedReader reader;
public ReaderThread(PipedReader reader){
this.reader = reader;
}
@Override
public void run(){
System.out.println("this is reader");
int receive = 0;
try{
while ((receive = reader.read())!= -1){
System.out.print((char)receive);
}
}catch (IOException e){
e.printStackTrace();
}
}
}
static class WriterThread implements Runnable{
private PipedWriter writer;
public WriterThread(PipedWriter writer){
this.writer = writer;
}
@Override
public void run(){
System.out.println("this is writer");
int receive = 0;
try {
writer.write("test");
} catch (IOException e) {
e.printStackTrace();
}finally {
try{
writer.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws IOException, InterruptedException {
PipedWriter writer = new PipedWriter();
PipedReader reader = new PipedReader();
writer.connect(reader);
new Thread(new ReaderThread(reader)).start();
Thread.sleep(1000);
new Thread(new WriterThread(writer)).start();
}
}
this is reader
this is writer
test
简单分析执行流程:
1:线程ReaderThread开始执行,
2:线程ReaderThread使用管道reader.read()进入阻塞
3:线程WriterThread开始执行
4:线程WriterThread执行writer.write(“test”),往管道写入字符串
5:线程WriterThread执行结束
6:线程ReaderThread读取管道输入的字符串并打印
7:线程ReaderThread执行结束。
其它通信相关
join方法
join()方法是Thread类的一个实例方法,作用是让当前线程陷入等待状态。等待join的线程执行完成再继续执行。比如:在主线程中要执行一个子线程,此子线程耗费的时间比较久,如果正常执行会在主线程完成之后完成,此时要是希望子线程先完成则可以在主线程中使用"子线程.join()",使主线程等待子线程完成之后再继续执行。
实例代码:
public class Join {
static class ThreadA implements Runnable{
@Override
public void run(){
System.out.println("我是子线程,我先等一会");
try {
Thread.sleep(1000);
System.out.println("ok了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadA());
thread.start();
thread.join();
System.out.println("如果我前面有人说话就代表使用了join()管用了,不然应该我先说话");
}
}
}
我是子线程,我先等一会
ok了
如果我前面有人说话就代表使用join()管用了,不然应该我先说话
sleep方法
sleep()是Thread类的一个静态方法,是让当前线程睡一会。
方法有:Thread.sleep(long),Thread.sleep(long,int)
注意:sleep方法是不会释放当前的锁的。而wait方法会。这也是常见的多线程的面试题。还有就是wait既可以指定时间也可以不指定时间,而sleep必须指定时间。
ThreadLocal类
它是一个本地线程副本变量工具类,内部是一个弱引用的Map来维护。它是让每个线程有自己独立的变量,线程之间互不影响。
如果开发者希望将类的某个静态变量与线程状态关联,则可以使用ThreadLocal。常见的使用场景为用来解决数据库连接,Session管理等。
InheritableThreadLocal
它不仅仅是当前线程可以存取副本值,而且它的子线程也可以存取这个副本值。