多线程
线程概述
几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。
线程的创建和启动
继承Thread类创建线程类
-
步骤:
-
1.定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就是线程需要完成的任务,因此run()方法称为线程执行体。
2.创建Thread子类的实例(创建线程对象)。
3.调用线程对象的start()方法来启动该线程。
public class EThread extends Thread{
private int i;
public EThread() {
}
public EThread(String name) {
super(name);
}
@Override
public void run() {
for(;i<100;i++) {
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20) {
new EThread().start();
new EThread().start();
}
}
}
}
使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。
进行多线程编程时不要忘记了java程序运行时默认的主线程,main()方法的方法体就是主线程的线程执行体。
实现Runnable接口创建线程类
-
步骤:
-
1.定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
2.创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。也可以在创建Thread对象时为该Thread对象指定一个名字。
3.调用线程对象的start()方法来启动该线程。
public class RThread implements Runnable{
private int i;
@Override
public void run() {
for(;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20) {
RThread rt = new RThread();
new Thread(rt,"线程1").start();
new Thread(rt,"线程2").start();
}
}
}
}
Runnable接口中只包含一个抽象方法,也就是说,Runnable接口是函数式接口,可使用Lambda表达式创建Runnable对象。
采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量。因为程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target,所以多个线程可以共享同一个线程类(实际上是线程的target类)的实例变量。
使用Callable和Future创建线程
通过实现Runnable接口创建多线程时,Thread类的作用就是把run()方法包装成线程执行体。
java5开始,java提供了Callable接口。Callable接口的call()方法可以有返回值、可以声明抛出异常。
java5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,FutureTask实现类不仅实现了Future接口,并还实现了Runnable接口,因此FutureTask可以作为Thread类的target。
-
步骤:
-
1.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例。Callable接口是函数式接口,可使用Lambda表达式创建Callable对象。
2.使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
3.使用FutureTask对象作为Thread对象的target创建并启动新线程。
4.调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
public class FutureTask_Callable implements Callable<Integer>{
private int i;
@Override
public Integer call() throws Exception {
for(;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
public static void main(String[] args) throws Exception {
FutureTask_Callable callable=new FutureTask_Callable();
FutureTask<Integer> task = new FutureTask<>(callable);
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20) {
new Thread(task,"有返回值的线程").start();
System.out.println("返回值为:"+task.get());
}
}
}
}
下面是利用lambda表达式创建Callable对象的
@Test
public void t1() throws Exception {
FutureTask<Integer> task = new FutureTask<>((Callable<Integer>)()->{
int i=0;
for(;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
});
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20) {
new Thread(task,"有返回值的线程").start();
System.out.println("返回值为:"+task.get());
}
}
}
调用FutureTask的get()方法来返回call()方法的返回值——该方法会导致主线程被阻塞,直到call()方法结束并返回为止。
创建线程的三种方式对比
通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。
-
采用实现Runnable、Callable接口的方式创建多线程的优缺点:
-
➢线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
➢在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
➢劣势是,编程稍稍复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
采用继承Thread类方式创建多线程的优缺点:
-
➢劣势是,因为线程类已经继承了Thread类,所以不能再继承其他父类。
➢优势是,编写简单,如果需要访问当前线程,无须使用Thread.currentThread()方法, 直接使用this即可获得当前线程。
线程的生命周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,他要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动后,它不可能一直"霸占"CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。
新建和就绪状态
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态。
当线程对象调用了start()方法后,该线程处于就绪状态,这个状态的线程并没有开始运行,只是表示该线程可以运行了,至于该线程何时开始运行,取决于JVM里线程调度器的调度。
运行和阻塞状态
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。
控制线程
join线程
Thread提供了让一个线程等待另一个线程完成的方法——join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。
线程睡眠:sleep
如果需要让当前正在执行的线程暂停一段时间并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。
线程同步
同步代码块
当有两个进程并发修改同一个文件时就有可能造成异常,java的多线程支持引入同步监视器来解决这个问题,使用同步监听器的通用方法就是同步代码块。
synchronized(obj){
……
//此处的代码就是同步代码块
}
synchronized后括号里的obj就是同步监视器,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
同步方法
同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于synchronized修饰的实例方法(非static方法)而言,无须显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。
synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、成员变量等。
同步锁(Lock)
从Java5开始,Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,同步锁由Lock对象充当。
Lock是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁)。Lock、ReadWriteLock是Java5提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。ReentrantLock对象锁可以显式地加锁、释放锁。
public class Account {
private String accountNo;
private double balance;
public Account() {}
public Account(String accountNo, double balance) {
super();
this.accountNo = accountNo;
this.balance = balance;
}
public double getBalance() {
return balance;
}
public String getAccountNo() {
return accountNo;
}
//定义锁对象
private final ReentrantLock lock=new ReentrantLock();
public void drawMoney(double money) {
lock.lock();
try {
if(balance>=money) {
System.out.println("取款操作成功!"+Thread.currentThread().getName()+"取出"+money+"元!");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance-=money;
System.out.println("\t"+Thread.currentThread().getName()+"余额为:"+balance);
}else {
System.out.println(Thread.currentThread().getName()+"你好,你的账户当前余额不足,无法取出人民币");
}
} finally {
lock.unlock();
}
}
使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。
使用Lock与使用同步方法有点相似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器,同样都符合"加锁→修改→释放锁"的操作模式。ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
LockSupport
LockSupport 和 CAS 是Java并发包中很多并发工具控制机制的基础,它们底层其实都是依赖Unsafe实现。
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞,LockSupport和每个使用它的线程都与一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit, 也就是将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。
park()和unpark()不会有 Thread.suspend 和 Thread.resume 所可能引发的死锁问题,由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。
如果调用线程被中断,则park方法会返回。同时park也拥有可以设置超时时间的版本。
LockSupport类是Java6(JSR166-JUC)引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:
//为指定线程提供“许可(permit)”
public native void unpark(Thread jthread);
/**
* 阻塞指定时间等待“许可”。
* @param isAbsolute: 时间是绝对的,还是相对的
* @param time:等待许可的时间
*/
public native void park(boolean isAbsolute, long time);
park()是使当前线程阻塞
unpark()是唤醒其他指定的线程
static Thread t1=null;
static Thread t2=null;
@Test
public void t18() {
char[] charArray = "1234".toCharArray();
char[] charArra2 = "ABCD".toCharArray();
t1=new Thread(()->{
for(char i:charArray) {
System.out.print(i+" ");
LockSupport.unpark(t2);
LockSupport.park();
}
});
t2=new Thread(()->{
for(char i:charArra2) {
LockSupport.park();
System.out.print(i+" ");
LockSupport.unpark(t1);
}
});
t2.start();
t1.start();
}
CAS机制
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
-
例子:
-
1.在内存地址V当中,存储着值为10的变量。
2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
7.线程1进行SWAP,把地址V的值替换为B,也就是12。
从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。
-
乐观锁:
- 每次去获取共享数据时会认为别人不会修改,所以不会上锁,但是在更新的时候会判断这期间有没有人去更新这个数据,判断方法有数据库的版本号机制、CAS算法实现。 悲观锁:
- 每次去获取共享数据时会认为别人也会修改,所以每次获取共享数据的时候会对它加一把锁,等你使用完了,释放了锁,再给别人使用数据。
所以java synchronized是重量级锁,也是重量级锁;
java中通过CAS的思想来实现的类都是乐观锁的机制,比如java.util.concurrent.atomic类;
数据库行锁属于悲观锁,数据库版本号属于乐观锁;
volatile相比synchronized是一种轻量级的同步机制。
原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。例如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。
使用atomic的包装类:
CAS机制不能保证代码块的原子性,CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
volatile
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
volatile只能保证变量的可见性,并不能保证变量的原子性。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
线程通信
wait()、notify()和notifyAll()
-
Object类提供的wait()、notify()和notifyAll()三个方法必须由同步监视器对象来调用:
-
1.对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
2.对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。
方法说明:
-
wait(): 导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。
notify(): 唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
notifyAll(): 唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
//账户类
public class Account {
private String accountNo;
private double balance;
private boolean flag=false;
public Account() {}
public Account(String accountNo,double balance) {
this.accountNo=accountNo;
this.balance=balance;
}
public double getBalance() {
return balance;
}
/*取钱方法:只有存一次才能取一次,不能连续取两次,取了一次后必须再存一次才能继续取*/
public synchronized void draw(double money) {
try {
if(!flag) {
//flag为假表明还没有存钱,当前线程进入等待并释放对当前对象的锁,执行其他线程
wait();
}else {
System.out.println(Thread.currentThread().getName()+"你好,已取走"+money);
balance-=money;
System.out.println("账户余额为"+balance);
flag=false;
//唤醒其他线程
notifyAll();
}
} catch (Exception e) {}
}
public synchronized void in(double money) {
try {
if(flag) {
wait();
}else {
System.out.println(Thread.currentThread().getName()+"存款"+money);
balance+=money;
System.out.println("账户余额为"+balance);
flag=true;
notifyAll();
}
} catch (Exception e) {
// TODO: handle exception
}
}
}
//取钱的线程类
public class DrawT extends Thread{
private Account account;
private double money;
public DrawT(String name,Account account, double money) {
super(name);
this.account = account;
this.money = money;
}
public void run() {
for(int i=0;i<100;i++) {
account.draw(money);
}
}
}
//存钱的线程类
public class InT extends Thread{
private Account account;
private double money;
public InT(String name,Account account, double money) {
super(name);
this.account = account;
this.money = money;
}
@Override
public void run() {
for(int i=0;i<100;i++) {
account.in(money);
}
}
}
@Test
public void t1() {
Account account = new Account("小汉",0);
new DrawT("大胸妹【取钱者】", account, 5000).start();
new InT("大吊哥[存钱者]", account, 5000).start();
new InT("小明哥[存钱者]", account, 5000).start();
new InT("胖虎哥[存钱者]", account, 5000).start();
}
最后被阻塞无法继续向下执行是因为3个存钱者线程一共有300次尝试存款操作,而1个取钱者线程只有100次尝试取钱操作,所以程序最后被阻塞。
如果把取钱线程的取钱操作次数修改为更大就不会造成最后存钱了没人取了:
public class DrawT extends Thread{
private Account account;
private double money;
public DrawT(String name,Account account, double money) {
super(name);
this.account = account;
this.money = money;
}
public void run() {
//这里循环次数修改为大于存钱者人数*100的数值即可
for(int i=0;i<305;i++) {
account.draw(money);
}
}
}
Condition控制线程通信
如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()、notifyAll()方法进行线程通信了。
当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。
Lock替代了同步方法或者同步代码块 , Condition替代了同步监视器的功能。Condition实例被绑定在一个Lock对象上面, 要获得Lock实例的Condition实例 , 调用Lock对象的newCondition()方法即可。
await(): 类似于隐式同步监视器上面的wait()方法,使当前线程等待,直到其他线程调用该Condition的signal()方法或者signalAll()方法来唤醒该线程。
signal(): 唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择唤醒任意一个。只有当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程。
signalAll(): 唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。
public class Account {
private String accountNo;
private double balance;
private boolean flag=false;
public Account() {}
public Account(String accountNo, double balance) {
super();
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public double getBalance() {
return balance;
}
//定义锁对象
private final ReentrantLock lock=new ReentrantLock();
//获得指定Lock对象对应的Condition
private final Condition cond=lock.newCondition();
public void draw(double money) {
lock.lock();
try {
if(!flag) {
//flag为假表明还没有存钱,当前线程进入等待并释放对当前对象的锁,执行其他线程
cond.await();
}else {
System.out.println(Thread.currentThread().getName()+"你好,已取走"+money);
balance-=money;
System.out.println("账户余额为"+balance);
flag=false;
//唤醒其他线程
cond.signalAll();
}
}finally{
lock.unlock();
}
}
public synchronized void in(double money) {
lock.lock();
try {
if(flag) {
cond.await();
}else {
System.out.println(Thread.currentThread().getName()+"存款"+money);
balance+=money;
System.out.println("账户余额为"+balance);
flag=true;
//唤醒其他线程
cond.signalAll();
}
} finally{
lock.unlock();
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((accountNo == null) ? 0 : accountNo.hashCode());
long temp;
temp = Double.doubleToLongBits(balance);
result = prime * result + (int) (temp ^ (temp >>> 32));
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Account other = (Account) obj;
if (accountNo == null) {
if (other.accountNo != null)
return false;
} else if (!accountNo.equals(other.accountNo))
return false;
if (Double.doubleToLongBits(balance) != Double.doubleToLongBits(other.balance))
return false;
return true;
}
}
线程池
使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个空闲的线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()或call()方法。
newFixedThreadPool(5):最多5个线程的线程池
newCachedThreadPool():足够多的线程,使任务不必等待
newSingleThreadExecutor():只有一个线程的线程池
-
使用线程池来执行线程任务步骤:
-
1.调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。
2.创建Runnable实现类或Callable实现类的实例,作为线程执行任务。
3.调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例。
4.当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。
@Test
public void t19() {
ExecutorService pool = Executors.newCachedThreadPool();
Runnable r = ()->{
for(int i=0;i<=2;i++) {
System.out.println(i);
}
};
Callable<Integer> c=()->{
Integer i=0;
for(;i<=2;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
};
for (int i = 0; i <= 1; i++) {
pool.submit(c);
}
}
ThreadLocal类
ThreadLocal,是Thread Local Variable(线程局部变量)的意思。就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。
同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源(变量)的竞争。