多线程
1:概念
进程和线程
一个任务通常就是一个程序,每个运行中的程序就是一个进程。线程是进程的执行单位,使得一个进程可以同时并发处理多个任务。当进程被初始化后,主线程就被创建了。对于绝大多数的进程来说通常仅要求有一个主线程。
并行性与并发性
并行:在同一时刻,有多条指令在多个处理器上同时执行。
并发:在同一时刻只有一条指令被执行,但由于多个进行指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
多线程的优势
当操作系统创建一个进程时,必须为该进程分配独立的内存空间与大量的相关资源。而同一个进程中的多个线程则共享进程代码块以及公有数据。
所以:进程之间不能共享资源,但线程之间共享资源很容易
创建线程的代价要小得多,因此使用多线程来实现多任务并发比多进程得效率要高。
Java语言内置了多线程功能支持,简化了多线程编程。
2.线程的创建和启动
1):继承Thread类创建线程类
public class ThreadDemo1 extends Thread{ int i = 0; //重写Thread类的run(),方法体就是线程需要完成的任务 public void run(){ for (int j = 0; j < 100; j++) { System.out.println(getName()+":"+j); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 20){ //创建线程对象并调用start()启动线程 new ThreadDemo1().start(); new ThreadDemo1().start(); } } } }
其实从结果就能看出来每次仅有一个线程被执行,由于速度快,宏观上来看就是多个线程一起执行。
也能看出来主线程和自行创建的新线程执行的不是同一个代码块:主线程执行的就是main()里的语句,新线程就是run()里的语句。
2):实现Runnable/Callable接口创建线程类
public Thread(Runnable target) { this(null, target, "Thread-" + nextThreadNum(), 0); }
创建线程只能通过new Thread的方式来创建,看上边的源码我们可以发现给一个Runnable实现类的实例也可以创建线程,Callable不行。但Callable的包装类FutureTask实现了RunnableFuture接口,RunnableFuture又继承了Runnable接口。所以通过Callable的包装类FutureTask也可创建线程。
这两者的区别是Callable允许抛出异常且可带返回值,通过FutureTask类的get()访问返回值。
public class BankTest { public static void main(String[] args) { Account a=new Account(0); //此处的b就是Runnable接口的实现类 Bank b=new Bank(a); Thread t1=new Thread(b); Thread t2=new Thread(b); t1.start(); t2.start(); } } class Account{ private double balance; public Account(double balance) { this.balance = balance; } public void deposit(double amt) { balance+=amt; System.out.println(Thread.currentThread().getName()+"存钱成功,余额为:"+balance); } } class Bank implements Runnable{ private Account act; public Bank(Account act) { this.act = act; } @Override public void run() { for (int i = 0; i < 3; i++) { act.deposit(1000); } } }
由结果我们可发现:采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量。
这里解释一下为什么可以共享实例变量的原因:Java堆中存放几乎所有的对象实例和数组,而且Java堆是一个共享的数据区域,所以实例变量可共享。
那么i为什么不能共享呢?首先run是一个方法,一个方法的开始与结束代表着虚拟机栈中的一个栈帧的入栈和出栈操作,且基本数据类型,引用类型以及返回值类型是存放在虚拟机栈的局部变量表中。虚拟机栈是线程私有的,所以不会共享i这个数据。
public class BankTest { public static void main(String[] args) { Account a=new Account(0); Bank b=new Bank(a); FutureTask task = new FutureTask(b); Thread t1=new Thread(task); Thread t2=new Thread(task); t1.start(); t2.start(); } } class Account{ private double balance; public Account(double balance) { this.balance = balance; } public void deposit(double amt) { balance+=amt; System.out.println(Thread.currentThread().getName()+"存钱成功,余额为:"+balance); } } class Bank implements Callable { private Account act; public Bank(Account act) { this.act = act; } @Override public Object call() throws Exception { for (int i = 0; i < 3; i++) { act.deposit(1000); } return 0; } }
3.线程的生命周期
1):新建:当使用new关键字创建了一个线程,该线程就处于新建状态。由Java虚拟机分配内存,并初始化其成员变量。
2):就绪:对象调用了start()后,处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,至于何时允许取决于JVM里线程调度器。
3):运行:处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,该线程就处于运行状态。
4):阻塞:(线程调用sleep()方法主动放弃所占用的处理器资源)(线程调用阻塞式IO方法,在该方法返回之前,该线程被阻塞)(线程试图获得同步监视器,但该监视器正被其他线程持有)(线程在等待某个通知)。以上的几种情况都会使得线程从运行状态到阻塞状态,再合适的时候会重新进入就绪状态。
5):死亡:(run()/call()方法执行完成)(抛出未捕获的Exception/Error)(直接调用线程的stop())都会使得线程从运行状态到死亡状态。
在这里我们需要注意,如果一个线程未调用其start()之前就调用了其run(),那么他不是一个线程,只是主线程执行了run()方法。
几个Thread类的方法:
静态:sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态。通常用来暂停程序的进行。
yield():强制当前线程回到就绪状态。
区别:sleep()暂停当前线程后会给其他线程机会,不会理会线程优先级。而yiled()只会给优先级相同或更高的线程。
sleep()要么捕获异常,要么抛出异常。而yiled()没有声明抛出任何异常。且sleep()比yiled()有更好的移植性。
实例:join()/join(long millis):当在某个线程执行流中调用其他线程的join()时,调用线程将被阻塞,直到join()方法加入的join线程执行完毕/执行到指定时间。
setDatemon(true):将指定线程设置为后台线程。JVM的垃圾回收机制就是典型的后台线程。当前台线程运行结束,无论后台进程是否运行结束,JVM会主动退出,所以后台线程也就被结束。
setPriority(int newPriority):设置线程优先级,参数可以是1-10之间的整数,也可使用提供的静态常量设置。
MAX_PRIORITY:10 MIN_PRIORITY:1 NORM_PRIORITY:5
4.线程的同步
当多个线程访问同一个数据时,可能会出现线程安全问题。如上边的银行存款问题:
解决的方法就是:当一个线程正在操作共享数据时,其他线程不能进来,直至该线程操作完成其他线程才可以进入。采用同步机制来解决。
1):同步代码块
public class BankTest { public static void main(String[] args) { Account a=new Account(0); Bank b=new Bank(a); new Thread(b).start(); new Thread(b).start(); } } class Account{ private double balance; private Object obj = new Object(); public Account(double balance) { this.balance = balance; } public void deposit(double amt){ //obj就是同步监视器,意思是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定 //Java程序允许使用任何对象作为同步监视器,但一般来说最好使用可能被并发访问的共享资源充当同步监视器。 //以下就是:加锁 -->修改 -->释放锁 的逻辑 synchronized (obj){ //被大括号包围起来的就是同步代码块 if (amt > 0) { balance += amt; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "存钱成功,余额为:" + balance); } //执行完同步代码块后,线程会释放对该同步监视器的锁定。 } }
通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码块,保证了线程的安全性
2):同步方法
使用synchronized关键字来修饰某实例方法,同步监视器就是this,即调用该方法的对象。线程安全类可以被多个线程安全的访问,且访问后都能地得到正确的结果。
public class BankTest { public static void main(String[] args) { Account a=new Account(0); Bank b=new Bank(a); new Thread(b).start(); new Thread(b).start(); } } class Account{ private double balance; public Account(double balance) { this.balance = balance; } public synchronized void deposit(double amt) { if (amt > 0) { balance += amt; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "存钱成功,余额为:" + balance); } } } class Bank implements Runnable{ private Account act; public Bank(Account act) { this.act = act; } @Override public void run() { for (int i = 0; i < 3; i++) { act.deposit(1000); } } }
该共享实例变量可被修改,说明这是一个可变类。可变类有两种运行环境:单线程,多线程。所以大多数情况下为可变类提供两种版本,在单线程环境中使用线程不安全版本保证其性能。例如:StringBuilder,StringBuffer:单线程下使用StringBuilder,多线程下使用StringBuffer。
3):同步锁(Lock)
Lock是空指多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。通常使用实现类ReentrantLock实现类定义Lock对象,必须显示打开或关闭锁。如下:
class A{ private final ReentrantLock lock = new ReentrantLock(); try{ lock.lock(); ... }finaly{ lock.unclock(); } }
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
死锁产生的四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
死锁产生原因:
1、竞争不可剥夺资源。如打印机
2、进程推进顺序不当。
6.线程通信:
让程序控制线程的轮换执行。有两种方式:1.同步监视器 2.Condition对象 3.阻塞队列
1):由同步监视器调用Object()类的这三个方法达到控制线程执行的作用 (synchronized)
Object类提供了三个方法:wait() notify() notifyAll()
wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()/notifyAll()方法来唤醒该线程。
notify():唤醒在此同步监视器上等待的单个线程。notifyAll()就是唤醒所有线程
举个例子:比如我们有两个线程,存钱和取钱,让他们轮番执行。
解决方法:定义一boolean型的变量flag初始化为false,如果存钱线程先进来:存钱成功,并让flag=true。如果下一个线程还是存钱那我们就调用同步监视器的wait()方法导致当前线程等待。这个时候另一个取钱线程就会占用cpu:先判断flag是否为true,为true说明有钱可取,那么我们就取钱成功,并唤起存钱线程......
2):使用Condition对象控制线程通信 (lock)
通过Lock对象的newCondition()方法可以得到一个Condition对象,Condition类主要提供了三个方法:await() singal() singalAll() ,与上边三个方法类似
3):使用阻塞队列控制线程通信
BlockingQueue是一个接口,其特征是:当线程试图想BlockingQueue中放入元素,如果该队列已满,则该线程被阻塞。取元素类似。一般用实现类达到我们想要的效果:ArrayBlockingQueue LinkedBlockQueue PriorityBlockingQueue SynchronousQueue DelayQueue
public class test { public static void main(String[] args) { ArrayBlockingQueue<Integer> bq = new ArrayBlockingQueue<>(1); Producer p = new Producer(bq); Consumer1 c = new Consumer1(bq); new Thread(p).start(); new Thread(c).start(); } } class Producer implements Runnable{ private BlockingQueue<Integer> bq; public Producer(BlockingQueue<Integer> bq) { this.bq = bq; } @Override public void run() { for (int i = 1; i < 101; i++) { try{ Thread.sleep(300); bq.put(i); System.out.println("生产了"+i+"号"); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Consumer1 implements Runnable{ private BlockingQueue<Integer> bq; public Consumer1(BlockingQueue<Integer> bq) { this.bq = bq; } @Override public void run() { try{ while (true){ Thread.sleep(400); //若队列为空,取操作将导致该线程阻塞 Integer take = bq.take(); System.out.println(Thread.currentThread().getName()+"取走了:"+take+" 号"); } } catch (InterruptedException e) { e.printStackTrace(); } } }