理解程序、进程、线程的概念
程序可以理解为静态的代码
进程可以理解为执行中的程序
线程可以理解为进程的进一步细分,程序的一条执行路径
使用多线程的优点:
提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
提高计算机系统CPU的利用率
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
在java中要想实现多线程,有两种手段,一种是继续Thread类,另外一种是实现Runable接口
继承java.lang.Thread类
下面来看一个简单的实例:
class Thread1 extends Thread{ private String name; public Thread1(String name) { this.name=name; } public void run() { for (int i = 0; i < 5; i++) { System.out.println(name + "运行 : " + i); try { sleep((int) Math.random() * 10); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Main { public static void main(String[] args) { Thread1 mTh1=new Thread1("A"); Thread1 mTh2=new Thread1("B"); mTh1.start(); mTh2.start(); } }
输出
输出: A运行 : 0 B运行 : 0 A运行 : 1 A运行 : 2 A运行 : 3 A运行 : 4 B运行 : 1 B运行 : 2 B运行 : 3 B运行 : 4 再运行一下: A运行 : 0 B运行 : 0 B运行 : 1 B运行 : 2 B运行 : 3 B运行 : 4 A运行 : 1 A运行 : 2 A运行 : 3 A运行 : 4
说明:
程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用Thread1的两个对象的start方法,另外两个线程也启动了,这样,整个应用就在多线程下运行。
以下是关系到线程运行状态的几个方法:
1)start方法
start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
2)run方法
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
3)sleep方法
sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
实现java.lang.Runnable接口
用Runnable也是非常常见的一种,我们只需要重写run方法即可。下面也来看个实例:
class Thread2 implements Runnable{ private String name; public Thread2(String name) { this.name=name; } @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(name + "运行 : " + i); try { Thread.sleep((int) Math.random() * 10); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Main { public static void main(String[] args) { new Thread(new Thread2("C")).start(); new Thread(new Thread2("D")).start(); } }
输出
输出: C运行 : 0 D运行 : 0 D运行 : 1 C运行 : 1 D运行 : 2 C运行 : 2 D运行 : 3 C运行 : 3 D运行 : 4 C运行 : 4
说明:
Thread2类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
Thread和Runnable的区别
对比一下继承的方式 vs 实现的方式
1.联系:public class Thread implements Runnable(继承的方式的Thread也实现了Runnable接口)
2.哪个方式好?
实现的方式优于继承的方式 why?
① 避免了java单继承的局限性
② 如果多个线程要操作同一份资源(或数据),更适合使用实现的方式
看一个例子:
//模拟火车站售票窗口,开启三个窗口售票,总票数为100张 //存在线程的安全问题 class Window extends Thread { int ticket = 100; public void run() { while (true) { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "售票,票号为:"+ ticket--); } else { break; } } } } public class TestWindow { public static void main(String[] args) { Window w1 = new Window(); Window w2 = new Window(); Window w3 = new Window(); w1.setName("窗口1"); w2.setName("窗口2"); w3.setName("窗口3"); w1.start(); w2.start(); w3.start(); } }
class Window implements Runnable { int ticket = 100;//要将全局变量声明为静态,不然每个对象都有这个属性,会卖出300张票 public void run() { while (true) { if (ticket > 0) { try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票号为:"+ ticket--); } else { break; } } } } public class Main { //模拟火车站售票窗口,开启三个窗口售票,总票数为100张 //存在线程的安全问题 public static void main(String[] args) { Window w1 = new Window(); Thread t1 = new Thread(w1, "t1"); Thread t2 = new Thread(w1, "t2"); Thread t3 = new Thread(w1, "t3"); t1.start(); t2.start(); t3.start(); } }
线程的生命周期
线程调度
1、调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY
线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY
分配给线程的默认优先级,取值为5。
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
2、线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
3、线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。
4、线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
5、线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
6、线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。
注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。
代码示例
join():指等待某线程终止。
为什么要用join()方法
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
class Thread1 extends Thread{ private String name; public Thread1(String name) { super(name); this.name=name; } public void run() { System.out.println(Thread.currentThread().getName() + " 线程运行开始!"); for (int i = 0; i < 5; i++) { System.out.println("子线程"+name + "运行 : " + i); try { sleep((int) Math.random() * 10); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + " 线程运行结束!"); } } public class Main { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()+"主线程运行开始!"); Thread1 mTh1=new Thread1("A"); Thread1 mTh2=new Thread1("B"); mTh1.start(); mTh2.start(); System.out.println(Thread.currentThread().getName()+ "主线程运行结束!"); } }
输出结果: main主线程运行开始! main主线程运行结束! B 线程运行开始! 子线程B运行 : 0 A 线程运行开始! 子线程A运行 : 0 子线程B运行 : 1 子线程A运行 : 1 子线程A运行 : 2 子线程A运行 : 3 子线程A运行 : 4 A 线程运行结束! 子线程B运行 : 2 子线程B运行 : 3 子线程B运行 : 4 B 线程运行结束! 发现主线程比子线程早结束
加join
public class Main { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()+"主线程运行开始!"); Thread1 mTh1=new Thread1("A"); Thread1 mTh2=new Thread1("B"); mTh1.start(); mTh2.start(); try { mTh1.join(); } catch (InterruptedException e) { e.printStackTrace(); } try { mTh2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+ "主线程运行结束!"); } }
运行结果: main主线程运行开始! A 线程运行开始! 子线程A运行 : 0 B 线程运行开始! 子线程B运行 : 0 子线程A运行 : 1 子线程B运行 : 1 子线程A运行 : 2 子线程B运行 : 2 子线程A运行 : 3 子线程B运行 : 3 子线程A运行 : 4 子线程B运行 : 4 A 线程运行结束! 主线程一定会等子线程都结束了才结束
yield
Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
class ThreadYield extends Thread{ public ThreadYield(String name) { super(name); } @Override public void run() { for (int i = 1; i <= 50; i++) { System.out.println("" + this.getName() + "-----" + i); // 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行) if (i ==30) { this.yield(); } } } } public class Main { public static void main(String[] args) { ThreadYield yt1 = new ThreadYield("张三"); ThreadYield yt2 = new ThreadYield("李四"); yt1.start(); yt2.start(); } }
运行结果:
第一种情况:李四(线程)当执行到30时会CPU时间让掉,这时张三(线程)抢到CPU时间并执行。
第二种情况:李四(线程)当执行到30时会CPU时间让掉,这时李四(线程)抢到CPU时间并执行。
sleep()和yield()的区别
sleep()和yield()的区别):sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。
线程的同步
1、线程安全问题存在的原因:
由于一个线程在操作共享数据过程中,未执行完毕的情况下,另外的线程参与进来,导致共享数据存在了安全问题。
2、如何解决线程安全问题
必须让一个线程操作共享数据完毕以后,其它线程才有机会参与共享数据的操作。
3、java如何实现线程安全:线程的同步机制
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码块(即为操作共享数据的代码)
}
1、共享数据:多个线程共同操作的同一个数据(变量)
2、同步监视器:由任何一个类的对象来充当。哪个线程获取此监视器,谁就执行大括号里被同步的代码。俗称:锁
注:在实现Runnable接口的方式中,考虑同步的话,可以使用this来充当锁。但是在继承的方式中,慎用this
class Window2 implements Runnable { int ticket = 1000;// 共享数据 public void run() { while (true) { synchronized (this) {//this表示当前对象,本题中即为w if (ticket > 0) { try { Thread.currentThread().sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--); } } } } } public class TestWindow2 { public static void main(String[] args) { Window2 w = new Window2(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
方式二:同步方法
将操作共享数据的方法声明为synchronized. 即此方法为同步方法,能够保证当其中一个线程执行此方法时,其他线程在外等待直至此线程执行完此方法。
同步方法的锁:this(不用显式的写)
class Window4 implements Runnable { int ticket = 1000;// 共享数据 public void run() { while (true) { show(); } } public synchronized void show() { if (ticket > 0) { try { Thread.currentThread().sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--); } } } public class TestWindow4 { public static void main(String[] args) { Window4 w = new Window4(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
练习
银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
class Account{ double balance;//余额 public Account(){ } //存钱 public synchronized void deposit(double amt){ notify(); balance+=amt; try { Thread.currentThread().sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":"+balance); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } class Customer extends Thread{ Account account; public Customer(Account account){ this.account=account; } public void run(){ for(int i=0;i<3;i++){ account.deposit(1000); } } } public class TestAccount { public static void main(String[] args) { Account acct = new Account(); Customer c1 = new Customer(acct); Customer c2 = new Customer(acct); c1.setName("甲"); c2.setName("乙"); c1.start(); c2.start(); } }
小结:
释放锁的操作
当前线程的同步方法、同步代码块执行结束
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
不会释放锁的操作
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
线程的死锁问题
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
解决方法:专门的算法、原则;尽量减少同步资源的定义
/死锁的问题:处理线程同步时容易出现。 //不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁 //写代码时,要避免死锁! public class TestDeadLock { static StringBuffer sb1 = new StringBuffer(); static StringBuffer sb2 = new StringBuffer(); public static void main(String[] args) { new Thread() { public void run() { synchronized (sb1) { try { Thread.currentThread().sleep(10);//问题放大 } catch (InterruptedException e) { e.printStackTrace(); } sb1.append("A"); synchronized (sb2) { sb2.append("B"); System.out.println(sb1); System.out.println(sb2); } } } }.start(); new Thread() { public void run() { synchronized (sb2) { try { Thread.currentThread().sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } sb1.append("C"); synchronized (sb1) { sb2.append("D"); System.out.println(sb1); System.out.println(sb2); } } } }.start(); } }
线程通信
wait() 与 notify() 和 notifyAll()
wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放),调用wait方法的一个或多个线程就会解除wait状态,重新参与竞争对象锁,程序如果可以再次得到锁,就可以继续向下运行。
1)wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。
2)当前线程必须拥有此对象的monitor(即锁),才能调用某个对象的wait()方法能让当前线程阻塞,
(这种阻塞是通过提前释放synchronized锁,重新去请求锁导致的阻塞,这种请求必须有其他线程通过notify()或者notifyAll()唤醒重新竞争获得锁)
3)调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;
(notify()或者notifyAll()方法并不是真正释放锁,必须等到synchronized方法或者语法块执行完才真正释放锁)
4)调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程,唤醒的线程获得锁的概率是随机的,取决于cpu调度
例子1(错误使用导致线程阻塞):三个线程,线程3先拥有sum对象的锁,然后通过sum.notify()方法通知等待sum锁的线程去获得锁,但是这个时候线程1,2并没有处于wait()导致的阻塞状态,而是在synchronized方法块处阻塞了,所以,这次notify()根本没有通知到线程1,2。然后线程3正常结束,释放掉sum锁,这个时候,线程1就立刻获得了sum对象的锁(通过synchronized获得),然后调用sum.wait()方法释放掉sum的锁,线程2随后获得了sum对象的线程锁(通过synchronized获得),这个时候线程1,2都处于阻塞状态,但是悲催的是,这之后再也没有线程主动调用sum.notify()或者notifyAll()方法显示唤醒这两个线程,所以程序阻塞.
public class CyclicBarrierTest { public static void main(String[] args) throws Exception { final Sum sum=new Sum(); new Thread(new Runnable() { @Override public void run() { try { synchronized (sum) { System.out.println("thread3 get lock"); sum.sum(); sum.notifyAll(); //此时唤醒没有作用,没有线程等待 Thread.sleep(2000); System.out.println("thread3 really release lock"); } } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { synchronized (sum) { System.out.println("thread1 get lock"); sum.wait();//主动释放掉sum对象锁 System.out.println(sum.total); System.out.println("thread1 release lock"); } } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { synchronized (sum) { System.out.println("thread2 get lock"); sum.wait(); //释放sum的对象锁,等待其他对象唤醒(其他对象释放sum锁) System.out.println(sum.total); System.out.println("thread2 release lock"); } } catch (Exception e) { e.printStackTrace(); } } }).start(); } } class Sum{ public Integer total=0; public void sum() throws Exception{ total=100; Thread.sleep(5000); } }
运行结果:
thread3 get lock thread3 really release lock thread2 get lock thread1 get lock //程序后面一直阻塞
例子2:还是上面程序,顺序不同,把线程3放到最下面。最后线程1,2都因为没有再次获得线程导致线程阻塞
运行过程:
线程1先运行获得sum对象锁(通过synchronized),但是随后执行了sum.wait()方法,主动释放掉了sum对象锁,然后线程2获得了sum对象锁(通过synchronized),也通过sum.wait()失去sum的对象锁,最后线程3获得了sum对象锁(通过synchronized),主动通过sum.notify()通知了线程1或者2,假设是1,线程1重新通过notify()/notifyAll()的方式获得了锁,然后执行完毕,随后线程释放锁,然后这个时候线程2成功获得锁,执行完毕。
public class CyclicBarrierTest { public static void main(String[] args) throws Exception { final Sum sum=new Sum(); new Thread(new Runnable() { @Override public void run() { try { synchronized (sum) { System.out.println("thread1 get lock"); sum.wait();//主动释放sum对象锁,等待唤醒 System.out.println(sum.total); System.out.println("thread1 release lock"); } } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { synchronized (sum) { System.out.println("thread2 get lock"); sum.wait(); //主动释放sum对象锁,等待唤醒 System.out.println(sum.total); System.out.println("thread2 release lock"); } } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { synchronized (sum) { System.out.println("thread3 get lock"); sum.sum(); sum.notifyAll();//唤醒其他等待线程(线程1,2) Thread.sleep(2000); System.out.println("thread3 really release lock"); } } catch (Exception e) { e.printStackTrace(); } } }).start(); } } class Sum{ public Integer total=0; public void sum() throws Exception{ total=100; Thread.sleep(5000); } }
执行结果
thread1 get lock thread2 get lock thread3 get lock thread3 really release lock 100 thread2 release lock 100 thread1 release lock
经典例题:生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
分析:
1、是否涉及到多线程的问题?是!生产者、消费者
2、是否涉及到共享数据?有!考虑线程安全问题
3、此共享数据是谁?产品的数量
4、是否涉及到线程的通信呢?存在生产者与消费者的通信
class Clerk{//店员 int product; public synchronized void addProduct(){//生产产品 if(product >= 20){ try { wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } }else{ product++; System.out.println(Thread.currentThread().getName() + ":生产了第" + product + "个产品"); notifyAll(); } } public synchronized void consumeProduct(){//消费产品 if(product <= 0){ try { wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } }else{ System.out.println(Thread.currentThread().getName() + ":消费了第" + product + "个产品"); product--; notifyAll(); } } } class Producer implements Runnable{//生产者 Clerk clerk; public Producer(Clerk clerk){ this.clerk = clerk; } public void run(){ System.out.println("生产者开始生产产品"); while(true){ try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } clerk.addProduct(); } } } class Consumer implements Runnable{//消费者 Clerk clerk; public Consumer(Clerk clerk){ this.clerk = clerk; } public void run(){ System.out.println("消费者消费产品"); while(true){ try { Thread.currentThread().sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } clerk.consumeProduct(); } } } public class TestProduceConsume { public static void main(String[] args) { Clerk clerk = new Clerk(); Producer p1 = new Producer(clerk); Consumer c1 = new Consumer(clerk); Thread t1 = new Thread(p1);//一个生产者的线程 Thread t3 = new Thread(p1); Thread t2 = new Thread(c1);//一个消费者的线程 t1.setName("生产者1"); t2.setName("消费者1"); t3.setName("生产者2"); t1.start(); t2.start(); t3.start(); } }