JAVA多线程编程实例详解
作者:doubly_yi
本文实例讲述了JAVA多线程编程。分享给大家供大家参考,具体如下:
- 进程是系统进行资源调度和分配的一个独立单位。
- 进程的特点
独立性:进程是系统中独立存在的实体,拥有自己的独立资源和私有空间。在没有经过进程本身允许的情况下,不能直接访问其他进程。
动态性:进程与程序的区别在于,前者是一个正在系统中活动的指令,而后者仅仅是一个静态的指令集合
并发性:多个进程可以在单个处理器上并发执行,而不受影响。
并发性和并行性的区别:
并行性:在同一时刻,有多条指令在多个处理器上同时执行(多个CPU)
并发性:在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果(单核)。
- 通过继承Thread类来创建并启动多线程
public class Deom_1 extends Thread{ public void run(){ super.run(); System.out.println("MyThread01"); } public static void main(String[] args){ Deom_1 demo=new Deom_1(); demo.setName("Demo_1"); demo.start(); System.out.println("当前线程的名字:"+Thread.currentThread().getName()+" 运行结束"); } }
多次调用start会抛出
java.lang.IllegalThreadStateException
异常
如果只调用run(),不调用start()方法,就相当于调用了一个普通的函数,实际上还是在同一个线程中运行的run()方法。
- 多线程编程时不要忘记了Java程序运行时默认的主线程,main方法的方法体就是主线程的线程执行体
- 使用继承Thread类的方法来创建线程类,多条线程之间无法共享线程的实例变量
- 实现Runnable接口创建线程类
//通过实现Runnable接口来创建线程类 public class SecondThread implements Runnable { private int i ; //run方法同样是线程执行体 public void run() { for ( ; i < 20 ; i++ ) { //当线程类实现Runnable接口时, //如果想获取当前线程,只能用Thread.currentThread()方法。 System.out.println(Thread.currentThread().getName() + " " + i); } } public static void main(String[] args) { for (int i = 0; i < 50; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 20) { SecondThread st = new SecondThread(); //通过new Thread(target , name)方法创建新线程 new Thread(st , "新线程1").start(); new Thread(st , "新线程2").start(); } } } }
-
采用实现继承方式的多线程:
编程相对简单
多条线程之间无法共享线程类实例变量
继承Thread类之后就不能继承其他的父类 -
采用实现接口的方式的多线程:
线程类只是实现了Runnable接口,还可以继承其他类
在这种方式下,可以多个线程共享同一个对象,非常适合多个相同线程来处理同一份资源的情况。
编程稍稍复杂一点 -
线程的生命周期:新建(NEW)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)
-
当程序使用new关键字创建一个线程后,该线程就处于新建状态;当线程对象调用了start()方法之后,该线程就处于就绪状态;如果处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体,则该线程就处于运行状态;但是它不可能一直处于运行状态,可能会被中断进入阻塞状态;线程运行结束后就会进入到死亡状态。
-
线程进入阻塞状态的情况:
线程调用了sleep方法主动放弃所占有的资源
线程调用了一个阻塞式IO方法,在该方法返回的时候被阻塞
线程尝试获取一个同步监听器,但是被其他线程占有
在等待某个通知
调用线程suspend方法挂起,不推荐使用容易造成死锁。 -
线程进入就绪状态的情况:
调用sleep方法的线程经过了指定时间
线程调用的阻塞式IO方法已经返回
线程成功获取试图取得同步监听器
线程在等待某个通知,其他线程发出一个通知
处于挂起状态的线程调用了resume恢复方法
进入阻塞状态的线程在获得执行机会后重新进入就绪状态,而不是运行状态。
- 线程进入死亡状态的情况:
run()方法执行完成,线程正常结束
线程抛出一个未捕获的异常或者错误
直接调用该线程的stop方法来结束该线程——该方法容易导致死锁,通常不推荐。
当主线程结束的时候,其他线程不受影响。一旦子线程启动它就拥有和主线程一样的地位。
-
不要试图对一个已经死亡的线程调用start()方法使它重新启动,会抛出
IllegalThreadStateException
异常。调用线程的isAlive()
方法可以测试某条线程是否死亡。 -
JAVA中控制线程的方法:
join线程
后台线程
线程睡眠:sleep
线程让步:yield
改变线程优先级 -
join线程
join()
:等待被join的线程执行完成
join(long millis)
:等待被join的线程时间最长millis毫秒
join(long millis, int nanos)
:等待被join的线程的时间最长millis毫秒加上nanos微秒 -
后台线程:任务是给其他线程提供服务,成为“后台线程”、“守候线程”。如果说有的前台线程死亡,后台线程会自动死亡。
调用Thread类的
setDaemon(true)
方法可以将指定线程设置为后台线程。该方法一定要在启动线程之前设置,否则会发生异常。同时提供isDaemon()
方法判断是否是后台线程。主线程一般默认为前台线程。前台线程创建的子线程默认是前台,后台线程创建的子线程默认是后台。
- 改变线程的优先级
PriorityTest low = new PriorityTest("低级"); low.start(); System.out.println("创建之初的优先级:" + low.getPriority()); //设置该线程为最低优先级 low.setPriority(Thread.MIN_PRIORITY);
PriorityTest high = new PriorityTest("高级"); high.start(); System.out.println("创建之初的优先级:" + high.getPriority()); //设置该线程为最高优先级 high.setPriority(Thread.MAX_PRIORITY);
每个线程的默认优先级都与创建它的父线程具有相同的线程,在默认的情况下,main线程具有普通优先级。
-
线程让步
yield()方法是Thread提供的一个静态方法,可以让当前正在执行的线程暂停转入就绪状态。等待下一次的重新调度。
实际上,当某个线程调用了yield()方法后只有优先级相同或者高于当前线程的其他就绪状态的线程才会获得执行的机会。 -
sleep和yield方法的区别
1、sleep方法暂停当前线程后,会给其他线程机会执行,不会理会其他线程的优先级。但是yield方法只会给优先级相同或者更高的线程。
2、sleep方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态。而yield方法直接转入就绪状态。
3、sleep方法会抛出InterruptedException
异常,所以调用时需要显示捕获异常,yield方法不会抛出任何异常。
4、sleep方法比yield方法具有更多的移植性,通常不依靠yield方法控制并发线程执行。 -
如果多个线程共同访问1个对象中的实例变量,则有可能出现”非线程安全”
class SelfPrivateNum { private int num = 0; public void addI(String username) { try { if (username.equals("a")) { num = 100; System.out.println("a set over!"); Thread.sleep(2000); } else { num = 200; System.out.println("b set over!"); } System.out.println(username + " num=" + num); } catch (InterruptedException e) { e.printStackTrace(); } } } class ThreadAA extends Thread { private SelfPrivateNum numRef; public ThreadAA(SelfPrivateNum numRef) { super(); this.numRef = numRef; } @Override public void run() { super.run(); numRef.addI("a"); } } class ThreadBB extends Thread { private SelfPrivateNum numRef; public ThreadBB(SelfPrivateNum numRef) { super(); this.numRef = numRef; } @Override public void run() { super.run(); numRef.addI("b"); } } public class RunUnsafe { public static void main(String[] args) { SelfPrivateNum numRef = new SelfPrivateNum(); ThreadAA athread = new ThreadAA(numRef); athread.start(); ThreadBB bthread = new ThreadBB(numRef); bthread.start(); } }
- 同步方法:就是使用
synchronized
关键字来修饰某个方法。当多个线程调用这个方法的时,以排队的方式进行处理。同步方法不具有继承性。
class SelfPrivateNum2 { private int num = 0; public synchronized void addI(String username) { try { if (username.equals("a")) { num = 100; System.out.println("a set over!"); Thread.sleep(2000); } else { num = 200; System.out.println("b set over!"); } System.out.println(username + " num=" + num); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
关键字synchronized取的是锁都是对象锁,而不是代码或者是方法当作锁。当多个线程访问的是同一个对象的同步方法的时候是排队的,而当多个线程访问多个对象的同步方法的时候运行的顺序是异步的。
- A线程先持有object对象的锁,B线程可以以异步的方式调用object对象总的非synchronizaed类型的方法
-
A线程先持有object对象的锁,B线程如果调用object的synchronizaed类型的方法则需要等待,也就是同步。
-
脏读
为了避免数据出现交叉的情况,使用synchronized关键字来进行同步。虽然在赋值时进行了同步,但是可能在取值的时候出现脏读(dirtyRead)的现象。发生脏读的情况是在读取实例变量时。出现脏读是应为getValue方法不是同步方法,解决方法可以定义为同步方法。
- synchronized方法是对当前对象进行加锁,而synchronized代码块是对某一个对象进行加锁。
//线程开始执行同步代码块之前,必须先获得对同步监控器的锁定 synchronized (Obj){ ……… //此处的代码就是同步代码块 }
通常使用可能被并发访问的共享资源充当同步监视器。
- 对同步监视器释放的情况:
当前线程的同步方法或代码块执行结束
当前线程的同步方法或代码块中遇到break、return终止了该代码块、方法的继续执行
当前线程的同步方法或代码块中出现未处理的Error或Exception
当前线程的同步方法或代码块中执行了同步监控器对象的wait()方法 -
以下情况不会释放同步锁:
当前线程的同步方法或代码块调用Thread.sleep()
,Thread.yield()
方法来暂停当前线程执行
当前线程的同步方法或代码块时,其他线程调用了该线程的suspend方法让该线程挂起 -
LOCK锁
Class A{ private final ReentrantLock lock= new ReentrantLock (); //需要保证线程安全的方法 public void m(){ //加锁 lock.lock(); try{ …………… } finally{ lock.unlock(); } } }
同步方法的比较
1、同步方法和同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在同一个块结构中,而且当获取多个锁的时候,他们必须按照相反的顺序依次释放。
2、Lock方法不仅可以使用与非结构快中,还可以试图中断锁和再次加锁的功能。被Lock加锁的代码中还可以嵌套调用。
3、资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍。
-
Volatile关键字
强制性从公共堆栈中(存放实例)中取得修饰的变量的值,而不是从线程的私有数据栈中取得变量的值。不能保证不会出现脏读的情况,需要用关键字synchronized
来解决。 -
ThreadLocal类
ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响。ThreadLocal提供资源的共享对象!
T get()
:返回此线程局部变量中当前线程副本的值
void remove()
:删除此线程局部变量中当前副本的值
void set(T value)
:设置此线程局部变量中当前线程副本的值
如果需要进行多个线程之间共享资源,以达到线程之间的通信功能,就使用同步机制;如果仅仅需要隔离多个线程之间的共享冲突,可以使Threadlocal。
-
死锁:当两个线程相互等待对象释放同步监视器的时候就会发生死锁。
使用一种称为资源排序的简单技术可以轻易避免死锁。
-
等待通知机制
方法wait()的作用是使当前执行代码线程进行等待,是Object类的方法,该方法用来将当前线程置于“阻塞队列”中,并在wait()所在的代码处停止执行,直到接到通知被唤醒。
在调用wait()
之前,线程必须持有该对象的对象级别锁,只能在同步方法或者是同步块中调用此方法。在执行wait方法后,当前线程释放锁。进入和其他线程竞争重新获得锁。如果调用wait方法没有持有锁,则会抛出异常。
方法notify()用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随便选择一个呈wait状态的线程。
方法nofity()
也是在同步方法或者同步块中调用,调用前同样要获得对象的对象级别所,否则抛出异常。在执行notify方法后,当前线程不会马上释放该对象锁,要等到执行notify方法的线程将程序执行完才能会释放锁。
方法notifyAll()方法可以使正在等待队列中等待同一共享资源的”全部”线程从等待状态退出,进入可运行状态。
public class Test3 { //main方法中有三个等待线程,一个唤醒线程,一个唤醒线程只能唤醒一个等待线程,程序出现阻塞 public static void main(String[] args) throws InterruptedException { Object lock = new Object(); ThreadA a = new ThreadA(lock); new ThreadA(lock).start(); new ThreadA(lock).start(); new ThreadA(lock).start(); Thread.sleep(1000); NotifyThread notifyThread = new NotifyThread(lock); notifyThread.start(); } } class ThreadA extends Thread { private Object lock; public ThreadA(Object lock) { super(); this.lock = lock; } @Override public void run() { Service service = new Service(); service.testMethod(lock); } } class NotifyThread extends Thread { private Object lock; public NotifyThread(Object lock) { super(); this.lock = lock; } @Override public void run() { synchronized (lock) { //notify()只能唤醒一个线程,nofiyAll()唤醒所有的等待线程 lock.notify(); // lock.notifyAll(); } } } class Service { public void testMethod(Object lock) { try { synchronized (lock) { System.out.println("begin wait() ThreadName=" + Thread.currentThread().getName()); lock.wait(); System.out.println(" end wait() ThreadName=" + Thread.currentThread().getName()); } } catch (InterruptedException e) { e.printStackTrace(); } } }
- 使用条件变量协调线程
在没有使用synchronized关键字保证同步,而采用Lock的程序。Java提供了condition类来保持协调。Lock替代synchronized,Condition替代同步监视器功能。
await
: 类似于wait。
signal
:唤醒在此Lock对象上等待的单个线程,选择是任意的。
signalAll
:唤醒在此Lock对象上等待的所有线程,选择是任意的。
public class Account { //显示定义Lock对象 private final Lock lock = new ReentrantLock(); //获得指定Lock对象对应的条件变量 private final Condition cond = lock.newCondition(); 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 void setAccountNo(String accountNo) { this.accountNo = accountNo; } public String getAccountNo() { return this.accountNo; } public double getBalance() { return this.balance; } public void draw(double drawAmount) { //加锁 lock.lock(); try { //如果账户中还没有存入存款,该线程等待 while(!flag) { cond.await(); } //执行取钱操作 System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount); balance -= drawAmount; System.out.println("账户余额为:" + balance); //将标识是否成功存入存款的旗标设为false flag = false; //唤醒该Lock对象对应的其他线程 cond.signalAll(); } catch (InterruptedException ex) { ex.printStackTrace(); } //使用finally块来确保释放锁 finally { lock.unlock(); } } public void deposit(double depositAmount) { lock.lock(); try { //如果账户中已经存入了存款,该线程等待 while(flag) { cond.await(); } //执行存款操作 System.out.println(Thread.currentThread().getName() + " 存款:" + depositAmount); balance += depositAmount; System.out.println("账户余额为:" + balance); //将标识是否成功存入存款的旗标设为true flag = true; //唤醒该Lock对象对应的其他线程 cond.signalAll(); } catch (InterruptedException ex) { ex.printStackTrace(); } //使用finally块来确保释放锁 finally { lock.unlock(); } } public int hashCode() { return accountNo.hashCode(); } public boolean equals(Object obj) { if (obj != null && obj.getClass() == Account.class) { Account target = (Account)obj; return target.getAccountNo().equals(accountNo); } return false; } }
使用notify()/notifyAll()方法进行通知时,被通知的线程却是JVM随机选择的。当notifyAll()通知所有WAITING线程,没有选择权,会出现相当大的效率问题。但是ReentrantLock结合Condition类可以”选择性通知”。Condition可以实现唤醒部分指定线程,这样有助于程序运行的效率。
- Callable和Future
从JDK1.5之后,Java提供了Callable接口,实际上就是Runnable接口的增强版。提供call方法作为线程执行体,但是功能更加强大。
Callable接口不是Runnable接口,不能直接作为Thread的target,有返回值得call方法也不能直接运行。这里需要一个包装器Future。
import java.util.concurrent.*; class RtnThread implements Callable<Integer> { //实现call方法,作为线程执行体 public Integer call() { int sum = 0; int i=0; for ( ; i < 10 ; i++ ) { System.out.println(Thread.currentThread().getName()+ " 的循环变量i的值:" + i); sum+=i; } //call()方法可以有返回值 return sum; } } public class CallableTest { public static void main(String[] args) { //创建Callable对象 RtnThread rt = new RtnThread(); //使用FutureTask来包装Callable对象 FutureTask<Integer> task = new FutureTask<Integer>(rt); for (int i = 0 ; i < 10 ; i++) { System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i); if (i == 5) { //实质还是以Callable对象来创建、并启动线程 new Thread(task , "有返回值的线程").start(); } } try { //获取线程返回值 System.out.println("子线程的返回值:" + task.get()); } catch (Exception ex) { ex.printStackTrace(); } } }
- 线程池
import java.util.concurrent.*; //实现Runnable接口来定义一个简单的 class TestThread implements Runnable { public void run() { for (int i = 0; i < 10 ; i++ ) { System.out.println(Thread.currentThread().getName() + "的i值为:" + i); } } } public class ThreadPoolTest { public static void main(String[] args) { //创建一个具有固定线程数(6)的线程池 ExecutorService pool = Executors.newFixedThreadPool(6); //向线程池中提交3个线程 pool.execute(new TestThread()); Future f1=pool.submit(new TestThread()); Future f2=pool.submit(new TestThread()); Future<Integer> f3=pool.submit(new RtnThread()); try { if(f1.get()==null&&f2.get()==null&&f3.get()!=null){ System.out.println("执行完毕!"); System.out.println(f3.get()); } //获取线程返回值 } catch (Exception ex) { ex.printStackTrace(); } //关闭线程池 pool.shutdown(); } }
更多java相关内容感兴趣的读者可查看本站专题:《Java进程与线程操作技巧总结》、《Java数据结构与算法教程》、《Java操作DOM节点技巧总结》、《Java文件与目录操作技巧汇总》和《Java缓存操作技巧汇总》
希望本文所述对大家java程序设计有所帮助。