Java多线程

一、线程和进程概述,

大部分时候,我们都做着单线程的编程,只有一条执行流,程序从main方法开始执行,依次向下执行每行代码,如果程序执行某行代码时遇到了阻塞,则程序就会停滞在该处。而多线程可以包含多个顺序执行流,多个顺序流之间互不干扰。

进程:

几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。

并发和并行的区别:

并行是指在同一时刻,有多条指令在多个处理器上同时执行。并发指,在同一时刻只能有一个条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。

多线程:

多线程是扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程也被称作轻量级进程,先策划那个是进程的执行单元。当进程被初始化后,主线程就被创建了。线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。

多线程的优势:

线程的划分程度要小于进程,使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

①、进程之间不能共享内存,但线程之间共享内存非常容易。

②、系统创建进程时,需要为该进程重新分配系统资源,但创建线程则代价小的多,因此使用多线程来实现多任务并发比多进程的效率高。

二、多线程的创建

1、继承Tread类创建线程

当Java程序开始运行后,程序至少会创建一个主线程,有mai()方法确定。

使用Thread类的方法创建线程类时,多个线程之间无法共享线程类的实例变量。

package com.edu.thread;
public class TestThread extends Thread{
 public void run(){
  for(int i=0;i<100;i++){
   System.out.println(this.getName()+":"+i);
  }
  
  
 }
 /**
  * @param args
  */
 public static void main(String[] args) {
  
  for(int i=0;i<100;i++){
   System.out.println(Thread.currentThread().getName()+":"+i);
   if(i==20){
    new TestThread().start();
    new TestThread().start();
   }
  }
 }
}

2、实现Runable接口创建线程类

package com.edu.thread;
public class TestRunnable implements Runnable{
 public void run(){
  for(int i=0;i<100;i++){
   System.out.println(Thread.currentThread().getName()+":"+i);
  }
  
  
 }
 /**
  * @param args
  */
 public static void main(String[] args) {
  TestRunnable run=new TestRunnable();
  for(int i=0;i<100;i++){
   System.out.println(Thread.currentThread().getName()+":"+i);
   if(i==20){
    new Thread(run).start();
    new Thread(run).start();
   }
  }
 }
}

使用Runnable实现多线程可以共享局部变量。

3、使用Callable和Future创建线程

从Java5开始,Java提供了Callable接口,该接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大。

call()方法有返回值。可以声明抛出异常。java5提供了Future接口来代表Callable接口里的call()方法的返回值,为Future提供了FutureTask实现类,该实现类实现了Future接口,并实现了Runnable。

package com.edu.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class TestCallable implements Callable{
 /**
  * @param args
  * @throws ExecutionException 
  * @throws InterruptedException 
  */
 public static void main(String[] args) throws InterruptedException, ExecutionException {
  TestCallable t=new TestCallable();
  FutureTask<Integer> task=new FutureTask<>(t);
  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());
 }
 @Override
 public Integer call() throws Exception {
  int i=0;
  for(;i<100;i++){
   System.out.println(Thread.currentThread().getName()+":"+i);
  }
  return i;
 }
}

三、线程的生命周期

当线程被创建后,要经过创建、就绪、运行、阻塞和死亡5中状态。

1、新建和就绪状态

当程序使用new关键字创建了一个进程后,该线程就处于新建状态,此时他和其他java对象一样,仅仅有Java虚拟机为其分配内存,并初始化其成员变量的值。当线程调用start()方法之后,该线程就处于就绪状态,处于这个状态的线程并没有运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。

2、运行和阻塞状态

如果出于就绪状态的线程获得了cpu,开始执行run()方法的线程执行体,则该线程处于运行状态。现在的桌面和服务器操作系统采用抢占式调度策略,当一个线程调用了sleep()方法和yield()后才会放弃所占用的资源。

当发生如下情况时,线程将会进入阻塞状态。

①线程调用sleep()方法主动放弃所占用的处理器资源。

②线程调用一个阻塞式io方法。

③线程试图获得一个同步监视器。

④线程在等待某个通知notify

⑤程序调用了线程的suspend()方法将该线程挂起,但这个方法容易导致思索。

3、线程死亡

一下几种情况,线程处于死亡状态

①run()或call()方法执行完成,线程正常结束

②线程抛出一个未捕获的Exception或Error。

③直接调用线程的stop(),该方法容易导致死锁。

可以使用isAlive()方法判断线程的状态,t当线程处于就绪、阻塞、运行三种状态时,该方法返回true。当线程处于新建、死亡时,该方法返回false。

注意:不要试图对一个已经死亡的线程调用start()方法,会引发IllegalThreadStateException异常。

四、控制线程

1、join进程

Thread提供了让一个线程等待另一个线程完成的方法-----join方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,知道被join()方法加入的join线程执行完为止。

2、后台线程

这种后台运行的线程的任务为其他的线程提供服务,这种线程被称为“后台线程”,。JVM垃圾回收线程就是典型的后台线程。

后台线程的特点是:如果所有的前台线程都死亡,后台线程会自动死亡。调用Thread对象的setDaemon()方法可将制定的线程设置成为后台线程。

3、线程睡眠sleep()方法

如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法。当当先线程调用sleep()方法进入阻塞状态后,在其睡眠时间短内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()方法中的线程也不会执行。

4、线程让步:yield()方法

yield()方法和sleep()方法有点类似,他也可以让当前执行的线程暂停,但是它不会阻塞该线程,而是将该线程转入就绪状态。yield()方法让当前线程暂停一下,让系统的线程器重新调度一下,完全可能,当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。

sleep()方法和yield()方法的区别:

①sleep方法暂停当前的线程后,会给其他线程执行机会,不会理会其他线程的优先级。而yield方法只给优先级相同或更高的线程的机会。

②sleep方法将使线程进入阻塞状态,而yield方法不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。

③sleep方法比yield方法有更好的可移植性,通常不建议使用yield()方法。

5、改变线程的优先级

每个线程默认的优先级都与创建它的父线程的优先级相同,main线程具有普通优先级,有main线程创建的子线程也具有普通优先级。通过Thread类的setPriority()方法指定优先级。

五、线程同步

1、线程代码块

run()方法的方法体不具有同步安全性,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通知方法就是同步代码块。格式如下synchronized(obj){  ......}

synchronized后括号里的obj就是同步监视器,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。

2、同步方法

同步方法就是使用synchronized关键字来修饰的某个方法。对于同步方法而言,无须显式指定同步监视器,同步方法的同步监视器是this,也就是该对象本身。不可变类总是线程安全的,因为它的对象状态不可改变。但可变对象需要额外的方法来保证其他线程安全。

3、释放同步监视器的锁定

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么合适会释放对同步监视器的锁定呢?

①当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。

②当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程会释放同步监视器。

③当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块,该方法异常结束时释放。

④执行wait()方法。

在如下情况下不会释放同步监视器:

①程序调用Thread.sleep()、Thread.yield()方法。

②程序调用suspend()方法。

4、同步锁Lock

 从Java5开始,Java提供了一种功能更强大的线程同步机制---通过显式定义同步锁对象来实现同步,在这种机制下,同步锁使用Lock对象来充当。Lock提供了比synchronized方法和代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。Lock、ReadWriteLock是Java提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类。使用该Lock对象可以显式地枷锁、释放锁,通常使用ReentrantLock的代码格式如下:

代码格式如下:

package com.edu.thread;
import java.util.concurrent.locks.ReentrantLock;
public class TestReentrantLock {
 
 private ReentrantLock lock=new ReentrantLock();
 public void m(){
  lock.lock();
  try{
   
  }finally{
   lock.unlock();
  }
 }
 /**
  * @param args
  */
 public static void main(String[] args) {
  // TODO Auto-generated method stub
 }
}

 

 使用ReentrantLock对象来进行同步,加锁和释放锁出现不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。

使用Lock与使用同步方法有点相似,只是使用Lock对象时显式地使用Lock对象作为同步锁,而使用同步方法时系统隐式的使用当前对象作为同步监视器。ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被枷锁的ReentrantLock锁再次加锁。

5、死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

六、线程通信

1、传统线程通信:wait()、notify()、notifyAll()

当线程在系统内部运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但我们可以通过一些机制来保证线程的协调运行。Object类提供了wait()、notify()、notifyAll()三个方法,这3个方法并不属于Thread类,而是属于Object类。这个3个方法必须有同步监视器对象来调用,可以分为以下2种情况:

①、对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这3个方法。

②、对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这3个方法。

wait()方法:导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法去唤醒该线程。

notify()方法:唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

notifyAll()方法:唤醒在此同步监视器上等待的所有线程。

package com.edu.thread;
class MyAccount{
 private String accountNo;
 private double balance;
 //标示是否存款,为true标示存款成功
 private boolean flag=false;
 public MyAccount(){}
 public MyAccount(String accountNo,double balance){
  this.accountNo=accountNo;
  this.balance=balance;
 }
 public String getAccountNo() {
  return accountNo;
 }
 public void setAccountNo(String accountNo) {
  this.accountNo = accountNo;
 }
 public double getBalance() {
  return balance;
 }
 public void setBalance(double balance) {
  this.balance = balance;
 }
 public boolean isFlag() {
  return flag;
 }
 public void setFlag(boolean flag) {
  this.flag = flag;
 }
 
 //取钱操作
 public synchronized void draw(double drawAmount){
  try{
   //没有存钱,则不能取钱
   if(!flag){
    wait();
   }
   else{
    System.out.println(Thread.currentThread().getName()+"取钱:"+drawAmount);
    balance-=drawAmount;
    System.out.println("账户余额为:"+balance);
    flag=false;
    notifyAll();
   }
  }catch(Exception e){
   e.printStackTrace();
  }
 }
 
 public synchronized void deposit(double depositAmount){
  try{
   //如果flag为真,说明已存款了,无需在存
   if(flag){
    wait();
   }else{
    System.out.println(Thread.currentThread().getName()+"存钱:"+depositAmount);
    balance+=depositAmount;
    System.out.println("账户余额为:"+balance);
    flag=true;
    notifyAll();
   }
   
  }catch(Exception e){
   e.printStackTrace();
  }
 }
 
}
class MyDrawThread extends Thread{
 private MyAccount account;
 private double drawAmount;
 public MyDrawThread(){}
 public MyDrawThread(String name,MyAccount account,double drawAmount){
  super(name);
  this.account=account;
  this.drawAmount=drawAmount;
 }
 public void run(){
  for(int i=0;i<100;i++){
   account.draw(drawAmount);
  }
 }
}
class MyDepositThread extends Thread{
 private MyAccount account;
 private double depositAmount;
 public MyDepositThread(){}
 public MyDepositThread(String name,MyAccount account,double depositAmount){
  super(name);
  this.account=account;
  this.depositAmount=depositAmount;
 }
 
 public void run(){
  for(int i=0;i<100;i++){
   account.deposit(depositAmount);
  }
 }
}
public class TestWait {
 /**
  * @param args
  */
 public static void main(String[] args) {
  MyAccount account=new MyAccount("11111", 0);
  new MyDrawThread("取钱者", account, 1000).start();
  new MyDepositThread("存钱者甲", account, 1000).start();
  new MyDepositThread("存钱者乙", account, 1000).start();
  new MyDepositThread("存钱者丙", account, 1000).start();
  
 }
}

 2、使用Condition控制线程通信

如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()、notifyAll()方法进行线程通信了。

当使用Lock对象来 保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象可以唤醒其他处于等待的线程。

Condition实例绑定在Lock对象上,调用Lock对象的newCondition()获得。Condition提供了3个方法:

await():类似wait()方法

signal():类似notify()

signalAll():类似notifyAll()

3、使用阻塞队列(BlockingQueue)控制线程通信

 

七、线程池:

 系统启动一个新线程的成本是比较高的,因为它涉及与操作系统计划。在这种情形下,使用线程池可以很好的提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行他们的run()方法或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()或call()方法。

线程池创建的几种方式:

1、Java5提供了一个Executors工厂类来产生线程池:

①newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。

②newFixedThreadPool(int nThreads):创建一个可重用的,具有固定线程数的线程池。

③newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于调用newFixedThreadPool()方法时传入参数为1。

④newScheduledThreadPool(int corePoolsize):创建具有指定数的线程池,它可以在指定延迟后执行线程任务。

⑤newSingleThreadScheduledExecutor():创建只有一个线程的线程池,他可以在制定延迟后执行线程任务。

前三个方法返回一个ExecutorService对象,该对象代表一个线程池,他可以执行Runable对象或Callable对象所代表的线程。后两个方法返回ScheduledExecutorService线程池,他是ExecutorService子类,它可以在指定延迟后执行线程任务。

ExecutorService对象提供了submit(Runnable task)等系列方法。当用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池的关闭序列,调用shutdown()方法后的线程池不再接受新任务,但会将以前所有已提交任务执行完成。当线程池中所有任务都执行完成后,池中的所有线程都会死亡。另外也可以调用线程池的shutdownNow()方法来关闭线程池,该方法试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。

package com.edu.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestExecutors implements Runnable{
 /**
  * @param args
  */
 public static void main(String[] args) {
  ExecutorService service=Executors.newFixedThreadPool(6);
  service.submit(new TestExecutors());
  service.submit(new TestExecutors());
  service.shutdown();
  
 }
 @Override
 public void run() {
  for(int i=0;i<100;i++){
   System.out.println(Thread.currentThread().getName()+"的i值为:"+i);
  }
  
 }
}

 2、Java7新增的ForkJoinPool

Java7提供的ForkJoinPool(不太明白写api的那个人,为啥不把名字起的好记一点叫什么ThreadPool之类的,费解),这个ForkJoinPool支持将一个任务拆分成多个“小任务”并行计算,再把多个小任务的结果合并成总的计算机结果。ForkJoinPool是ExecutorService的实现类。ForkJoinPool是一个抽象类,它还有两个抽子类:RecursiveAction和RecursiveTask。RecursiveTask代表由返回值,而RecursiveAction代表没有返回值的任务。

3、线程相关类

ThreadLocal类:

代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从而避免并发访问的线程安全问题。

 

 

 

 

转载于:https://my.oschina.net/wangning0535/blog/510093

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值