Java多线程

1. 基本概念

1.1 进程

进程是一个程序的运行状态和资源占用(内存,CPU)的描述
进程是程序的一个动态过程,它指的是从代码加载到执行完毕的一个完成过程
进程的特点:
a.独立性:不同的进程之间是独立的,相互之间资源不共享(举例:两个正在上课的教室有各自的财产,相互之间不共享)
b.动态性:进程在系统中不是静止不动的,而是在系统中一直活动的
c.并发性:多个进程可以在单个处理器上同时进行,且互不影响

1.2 线程

线程是进程的组成部分,一个进程可以有多个线程,每个线程去处理一个特定的子任务。
线程的执行是抢占式的,多个线程在同一个进程中可以并发执行,其实就是CPU快速的在不同的线程之间切换,也就是说,当前运行的线程在任何时候都有可能被挂起,以便另外一个线程可以运行。

1.3 线程和进程关系

a.一个程序运行后至少有一个进程
b.一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义的
c.进程间不能共享资源,但线程之间可以
d.系统创建进程需要为该进程重新分配系统资源,而创建线程则容易的多,因此使用线程实现多任务并发比多进程的效率高
e.系统创建进程需要为该进程重新分配系统资源,而创建线程则容易的多,因此使用线程实现多任务并发比多进程的效率高

2. 多线程实现

实现线程有两种方式:继承Thread类和实现Runnable接口

2.1 继承Thread类

Thread类是所有线程类的父类,实现了对线程的抽取和封装
继承Thread类创建并启动多线程的步骤:
a.定义一个类,继承自Thread类,并重写该类的run方法,该run方法的方法体就代表了线程需要完成的任务,因此,run方法的方法体被称为线程执行体
b.创建Thread子类的对象,即创建了子线程
c.用线程对象的start方法来启动该线程

public class MyThread extends Thread {
	@Override
	public void run() {
		System.out.println("我是MyThread的run方法");
		Thread thread = Thread.currentThread();// 获得当前线程
		System.out.println(thread.getName());
System.out.println(super.getName());
	}
}

测试方法:

public static void main(String[] args) {
	MyThread thread = new MyThread();
	thread.setName("子线程");// 设置线程名字
	thread.setPriority(10);// 设置线程优先级,但优先级高并不一定先执行。默认与父线程相同优先级
	thread.start();// 启动线程
	System.out.println("主线程");
}

使用线程的start()方法启动线程,而不能直接调用线程的run()方法,如果调用了run()方然,它就是个普通的对象方法了,而不是线程。线程之间征用资源,可以设置线程优先级,优先级1(最低)~10(最高),默认优先级是5。但并不能保证优先级高的一定会先执行,只是获得资源的概率大一些。

2.2 实现Runnable接口

实现Runnable接口创建并启动多线程的步骤:
a.定义一个Runnable接口的实现类,并重写该接口中的run方法,该run方法的方法体同样是该线程的线程执行体
b.创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象
c.调用线程对象的start方法来启动该线程

public class MyThread2 implements Runnable {
	@Override
	public void run() {
		System.out.println("我是MyThread2的run方法");
		Thread thread = Thread.currentThread();// 获得当前线程
		System.out.println(thread.getName());
	}
}

public static void main(String[] args) {
	MyThread2 my=new MyThread2();
	Thread thread=new Thread(my);//真正的线程对象
	thread.start();
	System.out.println("主线程");
}

2.3 两种方式比较

实现Runnable接口的方式:
a.线程类只是实现了Runnable接口,还可以继承其他类【一个类在实现接口的同时还可以继承另外一个类】
b.可以多个线程共享同一个target对象,所以非常适合多个线程来处理同一份资源的情况
c.弊端:编程稍微复杂,不直观,如果要访问当前线程,必须使用Thread.currentThread()

继承Thread类的方式:
a.编写简单,如果要访问当前线程,除了可以通过Thread.currentThread()方式之外,还可以使用super关键字
b.弊端:因为线程类已经继承了Thread类,则不能再继承其他类【单继承】
实际上大多数的多线程应用都可以采用实现Runnable接口的方式来实现【推荐使用匿名内部类】

Thread thread=new Thread(new Runnable(){
			@Override
			public void run() {
				
			}
		});

2.4 Callable和Future

无论是继承Thread类还是实现Runnable接口,都无法在执行任务之后获取到任务的结果,因为run()方法没有返回值。在java1.5之后提供了Callable和Future接口,在任务执行完毕后获取结果。

2.4.1 Callable接口

Callable的方法是有返回值的call()。它有一个泛型,代表返回值的类型:

import java.util.concurrent.Callable;

public class CallableTest implements Callable<Integer> {
    private int num;//指定sum,本线程用于计算1~num的和

    public CallableTest(int num) {
        this.num = num;
    }
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= num; i++) {
            sum += i;
        }
        return sum;
    }
}

测试方法:

public static void main(String[] args) {
    CallableTest thread = new CallableTest(100);
    //1.执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。
    FutureTask<Integer> result = new FutureTask<>(thread);
    new Thread(result).start();
    //2.接收线程运算后的结果
    try {
        Integer sum = result.get();
        System.out.println("sum=" + sum);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

也可以使用Executors

public static void main(String[] args) {
    CallableTest thread = new CallableTest(100);
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Future<Integer> result = executor.submit(thread);
    try {
        Integer sum = result.get();
        System.out.println("sum=" + sum);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    } finally {
        executor.shutdown();
    }
}

Callable+Future最终也是以Callable+FutureTask的方式实现的,executor的submit方法执行的是AbstractExecutorService的submit

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    return new FutureTask<T>(callable);
}

2.4.2 Future

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
Future有三个功能:

  1. 判断任务是否完成
  2. 获取任务执行结果
  3. 取消任务
public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException;
}

get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null
cancel()方法中的mayInterruptIfRunning表示是否可以打断已经执行的任务。本方法如果取消成功,就返回true,取消失败就返回false。

2.5 线程常用方法

2.5.1 线程休眠

Thread.sleep(毫秒数);可以让线程暂停一定时间,时间到了之后线程继续执行

2.5.2 线程合并

在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。

public static void main(String[] args) throws InterruptedException {
		MyThread thread = new MyThread();
		thread.start();// 启动线程
		/**
		 * join的意思是放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:
		 * 程序在main线程中调用thread线程的join方法,则main线程放弃cpu控制权,
		 * 并返回thread线程继续执行直到线程thread执行完毕
		 * 所以结果是thread线程执行完后,才到主线程执行,相当于在main线程中同步thread线程,thread执行完了,main线程才有执行的机会
		 */
		thread.join();
		System.out.println("主线程");
	}

join方法中如果传入参数,则表示这样的意思:如果A线程中调用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。join方法必须在线程start方法调用之后调用才有意义。

2.5.3 守护线程

隐藏起来一直在默默运行的线程,直到进程结束,又被称为守护线程或精灵线程,JVM的垃圾回收线程就是典型的后台线程
特征:如果所有的前台线程都死亡,后台线程会自动死亡,必须要在start之前执行。前台线程死亡后,JVM需要通知后台线程,后台线程接到指令并响应,需要一定的时间。main方法默认是前台线程。前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。

public class MyThread extends Thread {
	@Override
	public void run() {
		while(true){
			System.out.println(123);
		}
	}
}
public static void main(String[] args) throws InterruptedException {
	MyThread thread = new MyThread();
	thread.setDaemon(true);//设置成守护线程,如果不加这个,main退出后它还会一直运行
	thread.start();// 启动线程
	System.out.println("主线程");
}

2.5.4 线程让步

可以让当前正在执行的线程暂停,但它不会阻塞该线程,他只是将该线程转入就绪状态,完全可能出现的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行
实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程才会获得执行的机会

public class MyThread extends Thread {
	@Override
	public void run() {
		for(int i = 0;i < 50;i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
			if(i==20) {
				//线程让步,不会让线程进入阻塞状态
				Thread.yield();
			}
		}
	}
}
public static void main(String[] args) throws InterruptedException {
	MyThread thread1 = new MyThread();
	thread1.setName("t1");
	MyThread thread2 = new MyThread();
	thread2.setName("t2");
	thread1.start();// 启动线程
	thread2.start();// 启动线程
}

sleep和yield的区别:

  1. sleep暂停当前线程,会给其它线程执行的机会,不管其它线程优先级如何。yield方法只给相同或更高优先级的线程执行机会。
  2. sleep将线程进入阻塞状态,当sleep的时间过后进入就绪状态。而yield方法强制线程进入就绪状态。

2.6 线程生命周期

对于线程,当线程被创建并启动之后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,他会经历各种不同的状态【在一个进程中,多个线程同时运行,是在争抢CPU时间片】
New(新生):线程被实例化,但是还没有开始执行,就是刚new出来的。
Runnable(就绪):调用了start()方法,但还没有抢到CPU
Running(运行):抢到了CPU,CPU开始处理这个线程中的任务。当一条线程开始运行后,它不会一直处于运行状态(除非线程执行体足够短,瞬间就执行完了)。线程在运行的时候需要被中断,目的是让其它线程获得执行机会,线程的调度细节取决于底层平台的策略。对于抢占资源策略的系统,系统会给每个可执行的线程一个小时间段处理任务,当该事件段用完了,系统就会剥夺该线程所占用的资源,让下一个线程获得执行的机会。在选择下一个线程时,会考虑线程的优先级。
Blocked(阻塞):线程在执行过程中遇到特殊情况(如sleep),使得其他线程就可以获得执行的机会,被阻塞的线程会等待合适的时机重新进入就绪状态,注意是就绪状态不是运行状态。它必须等待调度器再次调用它才会进入运行状态。就绪和运行状态的转换不受认为控制,由系统调度完成。但一个情况除外,程序可以通过yield()方法让一个线程从运行状态到就绪状态。
Dead(死亡):线程终止
a.run方法执行完成,线程正常结束【正常的死亡】
b.直接调用该线程的stop方法强制终止这个线程
一个线程死亡后不能通过start()方法重新恢复,死亡就是死亡了。只能对新建状态的线程执行start()

3. 线程同步

当多个线程访问同一个资源的时候,可能会出现问题。如银行取款的时候,一个卡和一个存折操作的是同一个账户,张三拿着卡,张三老婆拿着存折,我们模拟这个过程:

public class Account {//账户
	private int amount = 0; 
	public int getAmount() {
		return amount;
	}

	public void setAmount(int amount) {
		this.amount = amount;
	}
}

取钱的线程:

public class DrawThread extends Thread {
	private Account account;// 取钱的时候操作一个账户
	private int money;// 每次取多少钱

	public DrawThread(Account account, String name, int money) {
		super(name);
		this.account = account;
		this.money = money;
	}

	public void run() {
		while (true) {// 一直取钱,目的是让多个线程一直执行,抢占CPU资源
			if (account.getAmount() >= money) {// 如果余额足够就取钱,假设每次取money
				System.out.println(super.getName() + "取钱" + money);
				try {
					Thread.sleep(1000);// 模拟银行系统卡顿的状况,或者银行职员正在数钱
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				account.setAmount(account.getAmount() - money);// 减掉账户余额
				System.out.println(super.getName() + "取款后余额为:" + account.getAmount());
			} else {
				System.out.println("余额不足" + account.getAmount());
				return;
			}
		}
	}
}

测试方法:

public static void main(String[] args) throws InterruptedException {
		Account account = new Account();
		account.setAmount(500);//开始有500块
		DrawThread card = new DrawThread(account, "张三", 100);
		DrawThread paper = new DrawThread(account, "老婆", 100);
		card.start();
		paper.start();
	}

运行结果(注意因为线程抢占资源是随机的,所以不一定是下面的顺序,多运行几次,看到余额是负数的情况就是我们想要的结果):

出现这种情况的原因,可能是

  1. 最后还剩下100块的时候,A线程判断余额足够,进入if块,然后sleep
  2. 在A线程sleep的期间内,B线程获得资源,此时A线程余额还没减掉,所以B判断余额足够,也进入if块。B线程sleep
  3. A线程sleep结束,重新获得资源,继续执行剩下的代码,减掉余额,余额是0
  4. B线程sleep结束,重新获得资源,继续执行剩下的代码,减掉余额,但刚刚A已经把余额减成0了,所以B减掉之后会变成-100
    不用纠结A和B谁是张三谁是老婆,最终的结果余额是负数 ,就不是我们期望看到的。
    解决这个问题,我们可以使用synchronized同步代码块或者方法。

3.1 同步代码块

synchronized(object){
  		//代码块
}

其中的object是同步监视器,任何时刻,只能有一条线程可以获得对同步监视器的锁定,当同步代码块结束后,该线程释放对同步监视器的锁定。

3.2 同步方法

除了同步代码块,我们还可以同步方法:

public class Account {
	private int amount = 0;

	public int getAmount() {
		return amount;
	}

	//同步方法,同一时刻只能有一个线程执行,同步监视器是this
	public synchronized void draw(int money) {
		if (amount >= money) {// 如果余额足够就取钱,假设每次取money
			System.out.println(Thread.currentThread().getName() + "取钱" + money);
			try {
				Thread.sleep(1000);// 模拟银行系统卡顿的状况,或者银行职员正在数钱
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			amount -= money;// 减掉账户余额
			System.out.println(Thread.currentThread().getName() + "取款后余额为:" + amount);
		} else {
			System.out.println("余额不足" + amount);
		}
	}
}

public class DrawThread extends Thread {
	private Account account;// 取钱的时候操作一个账户
	private int money;// 每次取多少钱

	public DrawThread(Account account, String name, int money) {
		super(name);
		this.account = account;
		this.money = money;
	}

	public void run() {
		for (int i = 0; i < 10; i++) {// 一直取钱,目的是让多个线程一直执行,抢占CPU资源
			account.draw(money);
		}
	}
}

3.3 同步锁

线程进入到同步代码块和同步方法之前,必须获得对同步监视器的锁定。程序无法显示的释放对同步监视器的锁定。线程会在如下几种情况下释放锁定:
a) 当线程的同步方法、同步代码块执行结束。当前线程会释放同步监视器。
b) 当前线程在同步代码块、同步方法中遇到break、return终止该代码块或方法的继续执行,当前线程会释放同步监视器。
c) 当前线程在同步代码块、同步方法中遇到未处理的Error或Exception,导致该代码块或方法异常结束的时候
d) 当前线程在同步代码块、同步方法中执行了同步监视器的wait()方法,则当前线程暂停,释放同步监视器。
以下情况不会释放同步监视器:
a) 当前线程在同步代码块、同步方法中,调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器
b) 线程执行同步代码块时,其它线程调用了该方法的suspend方法将该线程挂起,当前线程不会释放同步监视器。实际开发中应避免这种情况。
JDK1.5之后提供了同步锁Lock。它通过显式定义同步锁对象,实现同步。锁提供了对共享资源的独占访问。每次只能有一个线程对Lock对象加锁。
ReentrantLock是Lock的实现,称为可重入锁。

import java.util.concurrent.locks.ReentrantLock;

public class Account {
	private int amount = 1000;
	private final ReentrantLock lock = new ReentrantLock();//锁对象

	public int getAmount() {
		return amount;
	}

	public void draw(int money) {
		lock.lock();//加锁
		try {
			if (amount >= money) {// 如果余额足够就取钱,假设每次取money
				System.out.println(Thread.currentThread().getName() + "取钱" + money);
				try {
					Thread.sleep(1000);// 模拟银行系统卡顿的状况,或者银行职员正在数钱
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				amount -= money;// 减掉账户余额
				System.out.println(Thread.currentThread().getName() + "取款后余额为:" + amount);
			} else {
				System.out.println("余额不足" + amount);
			}
		} finally {
			lock.unlock();//释放锁
		}
	}
}

3.4 死锁

当两个线程同时等待对方释放同步监视器时就会发生死锁。

public class ClassA {

	public synchronized void methodA(ClassB b) {
		System.out.println(Thread.currentThread().getName() + "-methodA");
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+"要调用b.lastB()");
		b.lastB();
	}

	public synchronized void lastA() {
		System.out.println("lastA");
	}
}
public class ClassB {

	public synchronized void methodB(ClassA a) {
		System.out.println(Thread.currentThread().getName() + "-methodB");
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+"要调用a.lastA()");
		a.lastA();
	}

	public synchronized void lastB() {
		System.out.println("lastB");
	}
}
public class DeadLock implements Runnable {
	private ClassA a = new ClassA();
	private ClassB b = new ClassB();

	public void run() {
		a.methodA(b);
	}

	public void init() {
		b.methodB(a);
	}
}

public static void main(String[] args) throws InterruptedException {
		DeadLock thread = new DeadLock();
		new Thread(thread).start();
		thread.init();
	}

上面代码运行结果:

程序会卡住。

  1. 通过new Thread()启动的线程是Thread-0,主线程将thread当成普通对象,调用init()方法。
  2. 在init()方法中调用b.methodB()它是一个同步方法,所以主线程获得了b对象的锁。
  3. 接着b.methodB()中sleep的时候,Thread-0开始执行,调用了a.methodA,它也是个同步方法,Thread-0获得a的锁。
  4. 在a.methodA中调用b.lastB()需要获得B的锁,但此时b的锁在main手里。
  5. b.methodB()中sleep结束,开始调用a.lastA()需要a的锁,但此时a的锁在Thread-0手里,形成死锁。
    死锁形成的原因:
    1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
    2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
    3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
    4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
    破坏任何一个条件就能解除死锁。修改ClassA的代码:
public void methodA(ClassB b) {
		synchronized (this) {
			System.out.println(Thread.currentThread().getName() + "-methodA");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "要调用b.lastB()");
		}
		b.lastB();
	}

4. 线程通信

当线程在系统内运行时,程序无法准确控制线程轮换执行,但可以通过一些机制保证线程协调运行。

4.1 线程间协调运行

假设线程中有存款者和取款者两个线程。要存款者在指定账号中存入钱后,取款者立即取走钱;存款者不能连续存两次,取款者也不能连续取两次。两个线程交替运行。
实现线程间协调运行,可以借助Object类的wait(),notify()和notifyAll()。这是三个方法属于Object而不属于Thread。这三个方法必须由同步监视器对象来调用。
使用synchronized修饰的同步方法,同步监视器是该类的默认实例this,所以在同步方法中可以直接调用。
对于synchronized修饰的同步代码块,同步监视器是synchronized后面括号里的对象,所以必须由该对象调用者三个方法。
三个方法的作用:
wait():让当前线程等待,直到其它线程调用该同步监视器的notify()方法或notifyAll()唤醒。无参数的wail()会一直等待,带毫秒参数的是等待指定时间后自动苏醒。
notify():唤醒在此同步监视器上等待的单个线程。如果所有同步监视器都在这个线程上等待,会随意选择其中一条唤醒。只有当前线程放弃对同步监视器的锁定后(wait())才能执行被唤醒的线程。
notifyAll():唤醒在同步监视器上等待的所有线程。

public class Account {
	private int balance = 0;//账户余额
	public boolean flag = false;// false代表该存钱,true代表该取钱

	public synchronized void add(int money) {
		try {
			if (flag) {// 该取钱线程执行
				this.wait();// 当前线程等待
			}
			balance += money;
			System.out.println("存钱余额为" + balance);
			flag = true;//下次改取钱
			this.notifyAll();//唤醒取钱线程
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public synchronized void get(int money) {
		try {
			if(!flag){//该存钱执行,当前线程等待
				this.wait();
			}
			if (money <= balance) {
				balance -= money;
				System.out.println("取款余额为" + balance);
			} else {
				System.out.println("余额不足" + balance);
			}
			flag = false;//下次改存钱了
			this.notifyAll();//唤醒存钱线程
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
public static void main(String[] args) {
		final Account account = new Account();
		Thread add = new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 100; i++) {
					account.add(1);
				}
			}
		});
		Thread get = new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 100; i++) {
					account.get(1);
				}
			}
		});
		add.start();
		get.start();
	}

4.2 使用条件变量

当程序使用Lock而不是synchronized关键字同步的时候,不存在隐式的同步监视器,就不能使用wait、notify、notfyALL了。Java提供了一个Condition来保持协调。
Condition类提供了三个方法:
await():类似于wait()方法,导致当前线程等待。直到其它线程调用signal()或signalAll()方法来唤醒该线程。
signal():唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会任意选择唤醒其中一个线程。只有当前线程放弃对该Lock对象的锁定后(await)才会被唤醒。
signalAll():唤醒此Lock对象上等待的所有线程。

public class Account {
	private int balance = 0;// 账户余额
	public boolean flag = false;// false代表该存钱,true代表该取钱
	private final Lock lock = new ReentrantLock();
	private final Condition cond = lock.newCondition();

	public void add(int money) {
		lock.lock();
		try {
			if (flag) {// 该取钱线程执行
				cond.await();// 当前线程等待
			}
			balance += money;
			System.out.println("存钱余额为" + balance);
			flag = true;// 下次改取钱
			cond.signalAll();// 唤醒取钱线程
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void get(int money) {
		lock.lock();
		try {
			if (!flag) {// 该存钱执行,当前线程等待
				cond.await();
			}
			if (money <= balance) {
				balance -= money;
				System.out.println("取款余额为" + balance);
			} else {
				System.out.println("余额不足" + balance);
			}
			flag = false;// 下次改存钱了
			cond.signalAll();// 唤醒存钱线程
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
}

4.3 使用管道流(选学)

另一种线程间通信的方式是管道流,管道流有:
管道字节流:PipedInputStream、PipedOutputStream
管道字符流:PipedReader、PipedWriter
新IO流管道Channel:Pipe.SinkChannel、Pipe.SourceChannel
使用管道流实现多线程通信的步骤:

  1. 使用new操作符分别创建管道输入流和管道输出流
  2. 使用管道输入流或管道输出流的connect方法把两个输入流和输出流连接起来
  3. 将管道输入流、管道输出流分别传入两个线程
  4. 两个线程可以分别依赖各自的管道输入流、管道输出流进行通信
public class ReaderThread extends Thread {
	private PipedReader pipedReader;
	private BufferedReader bufferedReader;

	public ReaderThread() {
	}

	public ReaderThread(PipedReader pipedReader) {
		super();
		this.pipedReader = pipedReader;
		this.bufferedReader = new BufferedReader(pipedReader);
	}

	public void run() {
		String buf = null;
		try {
			while ((buf = bufferedReader.readLine()) != null) {
				System.out.println(buf);
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if (bufferedReader != null) {
					bufferedReader.close();
				}
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
	}
}
public class WriterThread extends Thread {
	private PipedWriter pipedWriter;

	public WriterThread() {
	}

	public WriterThread(PipedWriter pipedWriter) {
		this.pipedWriter = pipedWriter;
	}

	public void run() {
		try {
			pipedWriter.write("java基础入门\r\n");
			pipedWriter.write("多线程同步\r\n");
			pipedWriter.write("我爱编程\r\n");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if (pipedWriter != null)
					pipedWriter.close();
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
	}
}
public static void main(String[] args) {
		PipedWriter pw = null;
		PipedReader pr = null;
		try {
			pw=new PipedWriter();
			pr=new PipedReader();
			pw.connect(pr);
			new WriterThread(pw).start();
			new ReaderThread(pr).start();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

5. 线程组

ThreadGroup表示线程组,它可以对一批线程进行分类管理。默认情况下,子线程和创建它的父线程属于同一个线程组。只能在线程开始前设置线程组,直到该线程死亡,线程只能属于该线程组。

public class GroupTestThread extends Thread {
	public GroupTestThread(ThreadGroup group, String name) {
		super(group, name);
	}

	public void run() {
		while (!this.isInterrupted()) {//当线程没有被打断
			System.out.println(getName() + "线程运行");
		}
	}
}

public static void main(String[] args) throws InterruptedException {
		ThreadGroup group=new ThreadGroup("测试线程组");
		GroupTestThread t1=new GroupTestThread(group,"t1");
		GroupTestThread t2=new GroupTestThread(group,"t2");
		t1.start();
		t2.start();
		Thread.sleep(2000);
		group.interrupt();//组内线程会被打断
}

6. 线程池

系统启动一个新线程的成本较高,当程序需要创建大量的生命周期短暂的线程时,可以使用线程池。线程池在系统启动时就创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当run方法执行结束后,该线程不会死亡,而是再次返回线程池成为空闲状态,等待执行下一个Runnable对象。线程池可以有效的控制系统中并发线程的数量,控制并发线程不超过线程池要求的最大线程数。

public class PoolTestThread extends Thread {
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread() + "的i=" + i);
		}
	}
}
public static void main(String[] args) throws InterruptedException {
		ExecutorService pool = Executors.newFixedThreadPool(3);//如果超过3条,多余要等待执行
		pool.submit(new PoolTestThread());
		pool.submit(new PoolTestThread());
		pool.shutdown();
}

newFixedThreadPool(int num)同时最多有num条线程执行,超过限制的线程需要等待
newCachedThreadPool()可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们
newSingleThreadExecutor()线程会按照sumbit()的顺序依次执行
newScheduledThreadPool(int size)线程定时执行:
ScheduledExecutorService exec = Executors.newScheduledThreadPool(2);
MyThread t1 = new MyThread();
exec.execute(t1);// 将线程放入池中进行执行
MyThread t2 = new MyThread();
exec.schedule(t2, 1000, TimeUnit.MILLISECONDS);// 使用延迟执行风格的方法
exec.shutdown();

7. ThreadLocal

ThreadLocal线程局部变量,多个线程共享同一个对象的时候,线程局部变量属于单独的线程。

public class LocalTestObject {
	public ThreadLocal<String> name = new ThreadLocal<String>();

	public String getName() {
		return this.name.get();
	}

	public void setName(String str) {
		this.name.set(str);
	}
}
public class LocalTestThread extends Thread {
	private LocalTestObject object;

	public LocalTestThread(LocalTestObject object,String name) {
		super(name);
		this.object = object;
	}

	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName() + "中的name:" + object.getName());
			if (i == 5) {
				object.setName(getName()) ;
			}
		}
	}
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

专治八阿哥的孟老师

您的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值