Java中的多线程
1、线程的创建方式
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Section_VI {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread mt = new MyThread();
MythreadImplRunnable mir = new MythreadImplRunnable();
MyThreadImplCallable mic = new MyThreadImplCallable();
FutureTask<String> task = new FutureTask<String>(mic);
new Thread(mt,"线程1").start();
new Thread(mir,"线程2").start();
new Thread(task,"线程3").start();
Thread.sleep(1000);
if (task.isDone()){
System.out.println(task.get());
}
}
}
/**
* 继承Thread类创建线程任务
*/
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"第"+i+"次执行");
}
}
}
/**
* 实现Runnable接口创建线程任务
*/
class MythreadImplRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"第"+i+"次执行");
}
}
}
/**
* 实现Callable接口创建带返回值的线程任务
*/
class MyThreadImplCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"第"+i+"次执行");
}
return "MyThreadImplCallable线程任务已完成";
}
}
以上三种实现方式,都是可以创建出一个线程去执行相应的任务但也有不同点。
继承Thread类、实现Runnable接口、实现Callable接口这三种方法的区别在于:由于Java中不支持类之间多继承,当使用继承Thread类的方式创建线程时,就会导致该类无法继承其它类,去使用其它类中的方法。而实现Runnable接口的方式就可以做到,Java中接口之间支持使用多继承,如果一个类继承了另一个类,那么它可以通过实现Runnable接口从而做到既可以继承一个类,又能作为线程对象去使用。
Callable接口方式去创建的线程对象的特点在于,它可以在子线程运行时或运行结束后,有一个返回值返回到调用线程,调用线程可以根据这个返回值去做一些事情。这是以上两种方式都做不到的。
2、lambda表达式创建线程
JDK8特性之Lambda表达式实现线程创建
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* Lambda表达式简化线程的创建以及使用
*/
public class Section_VI {
public static void main(String[] args) throws ExecutionException, InterruptedException {
new Thread(()-> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"第"+i+"次执行");
}
},"线程1").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"第"+i+"次执行");
}
},"线程2").start();
FutureTask<String> task = new FutureTask<String>(()-> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"第"+i+"次执行");
}
return "MyThreadImplCallable线程任务已完成";
});
new Thread(task,"线程3").start();
Thread.sleep(1000);
if (task.isDone()){
System.out.println(task.get());
}
}
}
3、线程的状态
线程的整个生命周期经历的不同使用方式,可以定义为线程的状态,共分为六种状态。就绪、运行、阻塞、等待、计时等待、死亡
可以通过Thread.Stat进入源码中查看得出。
public enum State {
NEW, //就绪
RUNNABLE,//运行
BLOCKED,//阻塞
WAITING,//等待
TIMED_WAITING,//计时等待
TERMINATED;//死亡/停止
}
- 就绪:该状态表示,线程对象已经调用了start()方法,但是还没有抢到cpu时间片
- 运行:线程抢到了cpu时间片,进入cpu中运行
- 阻塞:由于某些原因,导致线程无法继续往下执行,线程就会被放置到等待区,这种情况大致有:线程往下执行时遇到了synchronized代码块而没有获得锁
- 等待:调用了wait()方法
- 计时等待:调用了sleep()方法时
- 死亡:线程的任务完成或者进行了return ; 操作。
4、线程的名称设置方式
通过查看Thread类的构造方法API,可以了解到在使用new Thread()创建线程对象时,该方法有多个重载可供选择,当我们需要给一个线程对象指定名称时,可以使用public Thread(Runnable target,String name)构造方法,target为线程任务对象,name就是线程名称,可以任意指定一个字符串为线程的名称。
new Thread(()->{
System.out.println("线程任务");
},"线程1").start();
5、线程的休眠
线程的休眠方法为静态方法,主要有两个:
- Thread.sleep(long millis) 当前正在执行的线程暂停执行指定的毫秒数
- Thread.sleeo(long millis,int nanos) 当前正在执行的线程暂停执行指定的毫秒数,加上指定的纳秒数
6、线程的阻塞
当多个线程并发执行,遇到了同一个同步代码块时,如果其中一个线程获得了该代码块的锁,那么,其它线程就会进入阻塞状态,一起等待占有锁的线程释放锁。
阻塞一般发生在线程阻塞于锁。
7、线程的中断
使用Thread.interrupted();方法时,会让当前线程中断进入阻塞状态
对于处于sleep,join等操作的线程,如果被调用interrupt()后,会抛出InterruptedException异常。
不可中断的操作,包括进入synchronized段以及Lock.lock(),inputSteam.read()等,调用interrupt()对于这几个问题无效,因为它们都不抛出中断异常。如果拿不到资源,它们会无限期阻塞下去。
对于Lock.lock(),可以改用Lock.lockInterruptibly(),可被中断的加锁操作,它可以抛出中断异常。等同于等待时间无限长的Lock.tryLock(long time, TimeUnit unit)。
对于inputStream等资源,有些(实现了interruptibleChannel接口)可以通过close()方法将资源关闭,对应的阻塞也会被放开。
一般情况下,线程退出可以使用while循环判断共享变量条件的方式,当线程内有阻塞操作时,可能导致线程无法运行到条件判断的地方而导致一直阻塞下去,这个时候就需要中断来帮助线程脱离阻塞。因此比较优雅的退出线程方式是结合共享变量和中断。
thread = new Thread(new Runnable() {
@Override
public void run() {
/*
* 在这里为一个循环,条件是判断线程的中断标志位是否中断
*/
while (flag&&(!Thread.currentThread().isInterrupted())) {
try {
Log.i("tag","线程运行中"+Thread.currentThread().getId());
// 每执行一次暂停40毫秒
//当sleep方法抛出InterruptedException 中断状态也会被清掉
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
//如果抛出异常则再次设置中断请求
Thread.currentThread().interrupt();
}
}
}
});
thread.start();
8、守护线程
守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默完成一些系统性的服务,比如垃圾回收线程,JIT线程就可以理解为守护线程。
与守护线程相对的是用户线程,用户线程可以认为是系统的工作线程,它会完成这个程序要完成的业务员操作。如果用户线程全部结束,则意味着这个程序无事可做。守护线程要守护的对象已经不存在了,那么整个应用程序就应该结束。因此,当一个Java应用内只有守护线程时,Java虚拟机自然退出。
可以通过Thread.setDaemon设置守护线程。
public class DaemonDemo {
public static void main(String[] args) {
Thread daemonThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
System.out.println("i am alive");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("finally block");
}
}
}
});
daemonThread.setDaemon(true);
daemonThread.start();
//确保main线程结束前能给daemonThread能够分到时间片
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
daemonThread.setDaemon(true)设置daemonThread为守护线程。
注意:守护线程必须在start之前设置,否则会报错。
9、保证线程安全的方式
保证线程安全主要有三种方式:
- 同步代码块:使用synchronized关键字修饰一个代码块,参数传入需要锁定的范围(类,对象)。一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。从而保证了线程的安全。当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。
- 同步方法:使用synchronized关键字修饰的方法为同步方法,它锁定的范围是该对象。如果该方法同时还是静态方法,那么锁定的就是这个类。
- 显示锁Lock:比较常用的为Lock类下的ReentrantLock( )类,它通过在需要进行同步的位置调用lock( )方法进行加锁,在同步位置的末尾进行unlock( )进行释放锁,从而保证线程安全。
10、死锁
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都是等待某个资源被释放,由于线程被无限期的阻塞,那么程序就不可能正常终止,这样的情况,就称为线程死锁。
死锁举例:
public class Section_VI_II {
/**
* 死锁演示 与 避免死锁演示
* @param args
*/
public static void main(String[] args) {
resource1 r1 = new resource1();
resource2 r2 = new resource2();
new Thread(()->{
synchronized (r1){
System.out.println(Thread.currentThread().getName()+"获取了r1,等待获取r2");
r1.print();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (r2){
System.out.println(Thread.currentThread().getName()+"获取了r2");
r2.print();
}
}
},"线程1").start();
new Thread(()->{
synchronized (r2){
System.out.println(Thread.currentThread().getName()+"获取了r2,等待获取r1");
r2.print();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (r1){
System.out.println(Thread.currentThread().getName()+"获取了r1");
r1.print();
}
}
},"线程2").start();
}
}
class resource1{
public void print(){
System.out.println("此时资源r1被"+Thread.currentThread().getName()+"占用了");
}
}
class resource2{
public void print(){
System.out.println("此时资源r2被"+Thread.currentThread().getName()+"占用了");
}
}
产生死锁的条件:
- 互斥条件,该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前,不能被其他线程强行抢夺,只有自己使用完毕才释放资源。
- 循环等待条件:若干进程之间形成了一种头尾相接的循环等待资源关系
通过以上的死锁条件得出,避免死锁的方法:
- 破环互斥条件:这个条件我们没有办法破坏,因为我们使用锁,就是想让它们互斥的(临界资源需要互斥访问)
- 破坏请求与保持条件:一次性申请全部资源
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环条件:靠顺序申请资源了预防,按某一顺序申请资源,释放资源则反序释放,破坏循环等待的条件。
11、多线程的通信
线程的通信主要使用的方法:wait() 与 notify() botifyAll()
wait( ) : 线程进入等待状态,并让出时间片。
notify( ) : 唤醒任意一个线程
notifyAll( ):唤醒全部阻塞队列中的线程,谁抢到时间片就谁就执行。
wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则,会出现IllegalMonitorStateException异常
12、经典问题:生产者与消费者问题
Synchronized 和 Lock的区别
1、Synchronized 是内置的java关键字,Lock是一个类
2、Synchronized 无法判断锁的状态,Lock可以判断是否获取了锁
3、Synchronized 会自动释放锁,Lock必须要手动释放锁,否则会出现死锁。
4、Synchronized
时,线程1获取了锁,线程2会等待,如果线程1出现了阻塞,那么线程2依然还在继续等待,Lock锁就不一定会等待下去。5、Synchronized 可重入锁,不可以中断且非公平。lock可重入锁,可以判断锁(有一个tryLock(
)方法,可以尝试获取锁),非公平(可以自己设置为公平)。6、适合少量的代码同步问题,Lock适合大量的同步代码。
java实现生产者与消费者问题(会出现虚假唤醒)
public class SynchronizedVersion {
//模拟生产者与消费者问题
public static void main(String[] args) {
resource r = new resource();
new Thread(()->{
try {
for (int i = 0; i < 30; i++) {
r.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程A").start();
new Thread(()->{
try {
for (int i = 0; i < 30; i++) {
r.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程B").start();
}
}
//资源类
class resource{
private int number = 0;
//对number进行+1
public synchronized void increment() throws InterruptedException {
//如果资源不为0 说明已经加过了,当前线程进入等待状态
if (number==1){
wait();
}
number ++;
System.out.println(Thread.currentThread().getName()+"->"+number);
this.notifyAll();
}
//对number进行-1
public synchronized void decrement() throws InterruptedException {
if (number==0)
wait();
number --;
System.out.println(Thread.currentThread().getName()+"->"+number);
this.notifyAll();
}
}
以上代码中,如果仅有两个线程时,程序正常运行。当线程数量大于2时,就会出现虚假唤醒的问题。
在代码
if (number==1){
wait();
}
中,如果同时出现两个线程进入if体内,进行wait操作时,此时如果其中一个线程被唤醒后,它并不会去判断此时的number是否依然是等于1的,而是直接进行加1操作,接着,有意思的事情就来了,该线程运行完了之后,如果把同为阻塞的另一个线程唤醒了,另一个线程依然不会进行number==1的判断,而进行了number++,那么,代码多线程中的运行就出现了问题。该问题成为虚假唤醒,那么怎么解决呢?
通过查看jdk文档,wait方法的描述中
synchronized( obj ){
while(条件){
obj.wait([毫秒数])
}
}
推荐使用while将wait方法包起来,这样就可以在线程被唤醒时,如果不满足条件,就依然执行wait操作,解决了虚假唤醒的问题。
正确的实现方法,if判断改成while判断:
public class SynchronizedVersion {
//模拟生产者与消费者问题
public static void main(String[] args) {
resource r = new resource();
new Thread(()->{
try {
for (int i = 0; i < 30; i++) {
r.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程A").start();
new Thread(()->{
try {
for (int i = 0; i < 30; i++) {
r.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程B").start();
new Thread(()->{
try {
for (int i = 0; i < 30; i++) {
r.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程C").start();
new Thread(()->{
try {
for (int i = 0; i < 30; i++) {
r.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程D").start();
}
}
//资源类
class resource{
private int number = 0;
//对number进行+1
public synchronized void increment() throws InterruptedException {
//如果资源不为0 说明已经加过了,当前线程进入等待状态
while (number==1){
wait();
}
number ++;
System.out.println(Thread.currentThread().getName()+"->"+number);
this.notifyAll();
}
//对number进行-1
public synchronized void decrement() throws InterruptedException {
while (number==0)
wait();
number --;
System.out.println(Thread.currentThread().getName()+"->"+number);
this.notifyAll();
}
}
JUC版实现
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class JUCVersion {
public static void main(String[] args) {
resource1 r1 = new resource1();
new Thread(()->{for (int i = 0; i < 10; i++)r1.increment();},"线程A").start();
new Thread(()->{for (int i = 0; i < 10; i++)r1.decrement();},"线程B").start();
}
}
class resource1{
private int number = 0;
private Lock lock = new ReentrantLock();
//使用Lock对象,创建监视器
Condition condition = lock.newCondition();
//对number进行+1
public void increment() {
//加锁
lock.lock();
try {
//如果资源不为0 说明已经加过了,当前线程进入等待状态
while (number==1){
//当前线程进入等待,调用Condition.await()将在等待之前以原子方式释放锁,并在等待返回之前重新获取锁。
condition.await();
}
number ++;
System.out.println(Thread.currentThread().getName()+"->"+number);
//唤醒全部等待线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
}
//对number进行-1
public void decrement(){
//加锁
lock.lock();
try {
//如果资源不为0 说明已经加过了,当前线程进入等待状态
while (number==0){
//当前线程进入等待
condition.await();
}
number --;
System.out.println(Thread.currentThread().getName()+"->"+number);
//唤醒全部等待线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
}
}
JUC拓展版,指定多个线程按顺序执行
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class JUCPlusVersion {
public static void main(String[] args) {
resource2 r2 = new resource2();
new Thread(() -> { for (int i = 0; i < 10; i++) r2.showA(); }, "线程A").start();
new Thread(() -> { for (int i = 0; i < 10; i++) r2.showB(); }, "线程B").start();
new Thread(() -> { for (int i = 0; i < 10; i++) r2.showC(); }, "线程C").start();
}
}
class resource2{
private int number = 1;
private Lock lock = new ReentrantLock();
//使用Lock对象,创建监视器
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void showA(){
lock.lock();
try {
while(number != 1){
//condition1进入等待
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"=>"+number);
number = 2;
//唤醒condition2
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}
public void showB(){
lock.lock();
try {
while(number != 2){
//condition2进入等待
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"=>"+number);
number = 3;
//唤醒condition3
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}
public void showC(){
lock.lock();
try {
while(number != 3){
//condition3进入等待
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"=>"+number);
number = 1;
//唤醒condition1
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}
}
13、线程池技术
线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
java中常用的线程池有:
Executors.newFixedThreadPool(nThreads):创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。Executors.newCachedThreadPool():创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
Executors.newSingleThreadExecutor():创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,
保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
Executors.newScheduledThreadPool(nThreads):创建一个定长线程池,支持定时及周期性任务执行。