简述
合理使用java多线程可以更好地利用服务器资源。一般来讲,线程内部有自己私有的线程上下文,互不干扰。但是当我们需要多个线程之间相互协作的时候,就需要我们掌握java线程的通信方式。
锁与同步
在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁,一个锁同一时间只能被一个线程持有。
在线程之间,有一个同步的概念,可以解释为:线程同步是线程之间按照一定的顺序执行。
public class Test {
public static Object lock = new Object();
public static void main(String[] args) throws InterruptedException{
new Thread(()->{
synchronized (lock){
for (int i = 0; i < 10; i++) {
System.out.println("Thread A:"+i);
}
}
},"Thread A").start();
Thread.sleep(1000);
new Thread(()->{
synchronized (lock){
for (int i = 0; i < 10; i++) {
System.out.println("Thread B:"+i);
}
}
},"Thread B").start();
}
}
这里声明了一个名字为lock的对象锁,在ThreadA和ThreadB内需要同步的代码块里,都是用synchronized关键字加上了同一个对象锁lock。
根据线程和锁的关系,同一时间只有一个线程持有一个锁,那么线程B就会等线程A执行完成后释放锁lock,线程B才能获得锁lock。
这里在主线程里使用sleep方法睡眠了10毫秒,是为了防止线程B先获得锁。因为如果同时start,线程A和线程B都是处于就绪状态,操作系统可能会先让B运行。这样就会先输出B的内容,然后B执行完成之后自动释放锁,线程A再执行。
等待/通知机制
上面一种基于“锁”的方式,线程需要不断地去尝试获取锁,如果失败了,再继续尝试。这可能会耗费服务器资源。
Java多线程的等待/通知机制是基于Object类的wait()方法和notify(),notifyAll()方法来实现的。
notify()方法会随机叫醒一个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程。
一个锁同一时刻只能被一个线程持有。假如线程A现在持有了一个锁lock并开始执行,它可以用lock.wait()让自己进入等待状态,这个时候,lock这个锁是被释放了的。这时,线程B获得了lock这个锁并开始执行,它可以在某一时刻,使用lock.notify(),通知之前持有lock锁并进入等待状态的线程A,说“线程A你不用等了,可以往下执行了”。
需要注意的是,这个时候线程B并没有释放锁lock,除非线程B这个时候使用lock.wait()释放锁,或者线程B执行结束自行释放锁,线程A才能得到lock锁。
示列代码:
public class Test {
public static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (lock){
for (int i = 0; i < 10; i++) {
System.out.println("Thread A:"+i);
try {
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"Thread A").start();
Thread.sleep(1000);
new Thread(()->{
synchronized (lock){
for (int i = 0; i < 10; i++) {
System.out.println("Thread B:"+i);
try {
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"Thread B").start();
}
}
需要注意的是等待/通知机制使用的是同一个对象锁,如果两个线程使用的是不同的对象锁,那它们之间是不能用等待/通知机制通信的。
信号量
JDK 提供了一个类似于“信号量”功能的类Semaphore。
这里介绍的一种基于volatile关键字实现的信号量通信。
volatile关键字能够保证内存的可见性,如果用volatile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的。
示列代码:
public class Test {
public static volatile int signal = 0;
public static void main(String[] args) throws InterruptedException {
//让线程A输出0,线程B输出1,线程A输出2,线程B输出3,依次类推
int b = 1;
new Thread(()->{
while (signal<10){
if(signal%2==0){
System.out.println("Thread A:"+signal);
signal++;
}
}
},"Thread A").start();
Thread.sleep(1000);
new Thread(()->{
while (signal<10){
if(signal%2==1){
System.out.println("Thread B:"+signal);
signal= signal + 1;
}
}
},"Thread B").start();
}
}
signal++并不是一个原子操作,所以在实际开发中,会根据需要使用synchronized给它上锁,或者使用AtomicInteger等原子类。并且上面的程序也并不是线程安全的,因为执行while语句后,可能当前线程就暂停等待时间片了,等线程醒来,可能signal已经大于等于5了。
信号量的应用场景
假如在一个停车场中,车位是我们的公共资源,线程就如同车辆,而看门的管理员就是起的“信号量”的作用。因为在这种场景下,多个线程(超过2个)需要相互合作,我们用简单的“锁”和“等待通知机制”就不那么方便了。这个时候可以用到信号量。
管道
管道是基于“管道流”的通信方式。JDK提供了PipedWriter、PipedReader、PipedOutputStream、PipedInputStream。其中,前面两个是基于字符的,后面两个是基于字节流的。
基于字符的示列代码:
public class Test {
public static void main(String[] args) throws InterruptedException, IOException {
PipedReader reader = new PipedReader();
PipedWriter writer = new PipedWriter();
//这里要连接才能通信
writer.connect(reader);
new Thread(()->{
System.out.println("this is read");
int receive = 0;
try {
while ((receive=reader.read())!=-1){
System.out.print((char) receive);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
},"Thread reader").start();
Thread.sleep(1000);
new Thread(()->{
System.out.println("this is write");
try {
writer.write("test");
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
},"Thread writer").start();
}
}
//输出
this is read
this is write
test
示列的代码的执行流程:
1.线程Thread reader开始执行
2.线程Thread reader使用管道reader.read()进入“阻塞”
3.线程Thread writer开始执行
4.线程Thread writer用writer.write(“test”)往管道写入字符串
5.线程Thread writer使用writer.close()结束管道写入,并执行完毕
6.线程Thread reader接收到管道输出的 字符串并打印
7.线程Thread reader执行完毕
管道通信的应用场景:
使用管道多半与I/O流相关。当一个线程需要向另一个线程发送一个信息(比如字符串或文件)时,就需要使用管道通信了。
其它通信相关
join方法
join()方法是Thread类的额一个实例方法。它的作用是让当前线程陷入“等待”状态,等join的这个线程执行完成后,再继续执行当前线程。
有时候,主线程创建并启动了子线程,如果子线程中需要进行大量的耗时运算,主线程往往早于子线程结束之前结束。
如果主线程想等待子线程执行完毕后,获得子线程的处理完的某个数据,就要用到join方法了。
示列代码:
public class Test {
public static void main(String[] args) throws IOException, InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("test");
});
thread.start();
thread.join();
System.out.println("如果没加join方法,会先被打印出来!");
}
}
-
注意join()方法有两个重载方法,一个是join(long millis),一个是join(long millis,int nanos)。
-
join()方法及其重载方法底层都是利用了wait(long)这个方法。
-
对于join(long millis,int nanos),通过查看源码(JDK1.8)发现,底层并没有精确到纳秒,而是对第二个参数做了简单的判断和处理
//源码 millis:毫秒 – 以毫秒为单位的等待时间 nanos: 0-999999额外的纳秒等待 public final synchronized void join(long millis, int nanos) throws InterruptedException { if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } if (nanos >= 500000 || (nanos != 0 && millis == 0)) { millis++; } join(millis); }
sleep方法
sleep方法是Thread类的一个静态方法。它的作用是让当前线程睡眠一段时间。它有这样两个方法:
- Thread.sleep(long)
- Thread.sleep(long,int)
区别:
- sleep方法不会释放当前的锁,而wait方法会
- wait可以指定时间,也可以不指定;而sleep必须指定时间
- wait释放CPU资源,同时释放锁;sleep释放CPU资源,但是不释放锁,所以容易死锁
- wait必须放在同步块或同步方法中;而sleep可以在任意位置