首页 > 代码库 > 黑马程序员_Java多线程
黑马程序员_Java多线程
- - - - - android培训、java培训、期待与您交流! - - - - - -
进程:正在进行中的程序。其实进程就是一个应用程序运行时的内存分配空间。进程负责的是应用程序的空间的标示。
线程:其实就是进程中一个程序执行控制单元,一条执行路径。线程负责的是应用程序的执行顺序。
- 一个进程至少有一个线程在运行,当一个进程中出现多个线程时,就称这个应用程序是多线程应用程序。
- 每个线程在栈区中都有自己的执行空间,自己的方法区、自己的变量。
jvm在启动的时,首先有一个主线程,负责程序的执行,调用的是main函数,主线程执行的代码都在main方法中。当产生垃圾时,收垃圾的动作,是不需要主线程来完成,因为这样主线程中的代码执行会停止,而去运行垃圾回收器代码,效率较低,所以由单独一个线程来负责垃圾回收。
随机性的原理:哪个线程获取到了cpu的执行权,哪个线程就执行,实质是cpu的快速切换造成。
返回当前线程的名称:Thread.currentThread().getName();线程的名称是由:Thread-编号定义的。编号从0开始。线程要运行的代码都统一存放在了run方法中。
线程要运行必须要通过类中指定的方法【start方法】开启。(启动后,就多了一条执行路径)
start方法:
- 启动了线程
- 让jvm调用了run方法。
创建线程的第一种方式:继承Thread ,由子类复写run方法:
步骤:
- 定义类继承Thread类;
- 目的是复写run方法,将要让线程运行的代码都存储到run方法中;
- 通过创建Thread类的子类对象,创建线程对象;
- 调用线程的start方法,开启线程,并执行run方法。
class Show extends Thread { public void run() { for (int i =0;i<5 ;i++ ) { System.out.println(name +"_" + i); } } public Show(){} public Show(String name) { this.name = name; } private String name; public static void main(String[] args) { new Show("csdn").start(); new Show("黑马").start(); } }
【可能的运行结果】:
为什么我们不能直接调用run()方法呢?原因是线程的运行需要本地操作系统的支持。
查看start的源代码发现:
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } } private native void start0();
这个方法用了native关键字,native表示调用本地操作系统的函数,多线程的实现需要本地操作系统的支持。
线程状态:
- 被创建:start()
- 运行:具备执行资格,同时具备执行权;
- 冻结:sleep(time),wait()—notify()唤醒;线程释放了执行权,同时释放执行资格;
- 临时阻塞状态:线程具备cpu的执行资格,没有cpu的执行权;
- 消亡:stop()
创建线程的第二种方式:实现一个接口Runnable:
步骤:
- 定义类实现Runnable接口。
- 覆盖接口中的run方法(用于封装线程要运行的代码)。
- 通过Thread类创建线程对象;
- 将实现了Runnable接口的子类对象作为实际参数传递给Thread类中的构造函数。【为什么要传递呢?因为要让线程对象明确要运行的run方法所属的对象】
- 调用Thread对象的start方法,开启线程,并运行Runnable接口子类中的run方法。
Ticket t = new Ticket();
直接创建Ticket对象,并不是创建线程对象。【因为创建线程对象只能通过new Thread类,或者new Thread类的子类才可以】
Thread t1 = new Thread(t); //创建线程。
只要将t作为Thread类的构造函数的实际参数传入即可完成线程对象和t之间的关联。【为什么要将t传给Thread类的构造函数呢?其实就是为了明确线程要运行的代码run方法】
t1.start();//开启线程
为什么要有Runnable接口的出现?
1:通过继承Thread类的方式,可以完成多线程的建立。但是这种方式有一个局限性,如果一个类已经有了自己的父类,就不可以继承Thread类,因为java单继承的局限性。
可是该类中的还有部分代码需要被多个线程同时执行,这时怎么办呢?
只有对该类进行额外的功能扩展,java就提供了一个接口Runnable。这个接口中定义了run方法,其实run方法的定义就是为了存储多线程要运行的代码。
所以,通常创建线程都用第二种方式。【因为实现Runnable接口可以避免单继承的局限性】
2:其实是将不同类中需要被多线程执行的代码进行抽取。将多线程要运行的代码的位置单独定义到接口中。为其他类进行功能扩展提供了前提。
所以Thread类在描述线程时,内部定义的run方法,也来自于Runnable接口。
实现Runnable接口可以避免单继承的局限性。而且,继承Thread,是可以对Thread类中的方法,进行子类复写的。但是不需要做这个复写动作的话,只为定义线程代码存放位置,实现Runnable接口更方便一些。所以Runnable接口将线程要执行的任务封装成了对象。
new Thread(new Runnable(){ //匿名public void run(){ System.out.println("runnable run");}}){public void run(){ System.out.println("subthread run");}}.start(); //结果:subthread run
Try {Thread.sleep(10);}catch(InterruptedException e){}// 当刻意让线程稍微停一下,模拟cpu 切换情况。
Thread和Runnable的区别:
如果一个类继承Thread,则不能资源共享(有可能是操作的实体不是唯一的);但是如果实现了Runable接口的话,则可以实现资源共享。
class Show implements Runnable { private int count = 10;//假设有10张票 @Override public void run() { for (int i = 0; i < 5 ; i++ ) { if (this.count > 0) { System.out.println(Thread.currentThread().getName()+"正在卖票" + this.count--); } } } public static void main(String[] args) { Show s = new Show(); //注意必须保证只对1个实体s操作 new Thread(s,"窗口1").start(); new Thread(s,"窗口2").start(); new Thread(s,"窗口3").start(); } }
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免java中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
设置线程优先级
Thread t = new Thread(myRunnable); t.setPriority(Thread.MAX_PRIORITY);//一共10个等级,Thread.MAX_PRIORITY表示最高级10 t.start();
//MAX_PRIORITY : 其值是 10
//MIN_PRIORITY : 其值是 1
//NORM_PRIORITY: 其值是 5
提示:主线程的优先级是5,不要误以为优先级越高就先执行。谁先执行还是取决于谁先去的CPU的资源。
控制线程的方法:
- join方法:假如你在A线程中调用了B线程的join方法B.join();,这时B线程继续运行,A线程停止(进入阻塞状态)。等B运行完毕A再继续运行。
- sleep方法:线程中调用sleep方法后,本线程停止(进入阻塞状态),运行权交给其他线程。
- yield方法:线程中调用yield方法后本线程并不停止,运行权由本线程和优先级不低于本线程的线程来抢。(不一定优先级高的能先抢到,只是优先级高的抢到的时间长)
package heimablog;// 定义Runnable 接口的实现类public class JoinTest implements Runnable { // 重写run() 方法 public void run() { for (int i = 0; i <= 10; ++i) System.out.println(Thread.currentThread().getName() + "..." + i); } public static void main(String args[]) throws InterruptedException { // 创建Runnable 接口实现类的实例 JoinTest t = new JoinTest(); // 通过 Thread(Runnable target) 创建新线程 Thread thread = new Thread(t); Thread thread1 = new Thread(t); // 启动线程 thread.start(); // 只有等thread 线程执行结束,main 线程才会向下执行; thread.join(); System.out.println("thread1 线程将要启动"); thread1.start(); }}
- wait方法:当前线程转入阻塞状态,让出cpu的控制权,解除锁定。
- notify方法:唤醒因为wait()进入阻塞状态的其中一个线程。
- notifyAll方法: 唤醒因为wait()进入阻塞状态的所有线程。
这三个方法都必须用synchronized块来包装,而且必须是同一把锁,不然会抛出java.lang.IllegalMonitorStateException异常。
多线程安全问题的原因:
发现一个线程在执行多条语句时,并运算同一个数据时,在执行过程中,其他线程参与进来,并操作了这个数据,导致了错误数据的产生。
涉及到两个因素:
- 多个线程在操作共享数据。
- 有多条语句对共享数据进行运算。
如下面程序:
package heimablog;/* * 多个线程同时访问一个数据时,出现的安全问题。 * 模拟一个卖火车票系统:一共有100张票,多个窗口同时卖票 */class Ticks implements Runnable { private int ticks = 100 ; public void run() { while (ticks > 0) { //加入sleep 方法是为了更明显的看到该程序中出现的安全问题。 try{Thread.sleep(10);}catch (Exception e) {} System.out.println(Thread.currentThread().getName() +"...卖出了第"+ticks+"张票"); ticks -- ; } }}public class ShowTest { public static void main(String args[]) { //创建 Runnable 实现类 Ticks 的对象。 Ticks t = new Ticks() ; //开启4个线程处理同一个 t 对象。 new Thread(t , "一号窗口").start() ; new Thread(t , "二号窗口").start() ; new Thread(t , "三号窗口").start() ; new Thread(t , "四号窗口").start() ; }}
运行的结果会出现"一号窗口...卖出了第0张票,四号窗口...卖出了第-1张票,二号窗口...卖出了第-2张票"这样的安全问题。
解决安全问题的原理:
只要将操作共享数据的语句在某一时段让一个线程执行完,在执行过程中,其他线程不能进来执行就可以解决这个问题。
解决这类问题的方法:
1、同步代码块。
2、同步函数。
同步代码块
synchronized(obj) // 任意对象都可以,这个对象就是锁。{ //此处的代码就是同步代码块,也就是需要被同步的代码;}
synchronized 后括号里面的 obj 就是同步监视器。代码含义:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。即只有获得对同步监视器的锁定的线程可以在同步中执行,没有锁定的线程即使获得执行权,也不能在同步代码块中执行。
注意:虽然JAVA 程序允许使用任何对象来作为同步监视器。但是还是推荐使用可能被并发访问的共享资源来充当同步监视器。
修改代码如下:
package heimablog;/* * 多个线程同时访问一个数据时,出现的安全问题。 * 模拟一个卖火车票系统:一共有100张票,多个窗口同时卖票 */class Ticks implements Runnable { private int ticks = 100; public void run() { while (ticks > 0) { synchronized (Ticks.class) { if (ticks > 0) { try { Thread.sleep(10); } catch (Exception e) {} System.out.println(Thread.currentThread().getName() + "...卖出了第" + ticks + "张票"); ticks--; } } } }}public class ShowTest { public static void main(String args[]) { // 创建 Runnable 实现类 Ticks 的对象。 Ticks t = new Ticks(); // 开启4个线程处理同一个 t 对象。 new Thread(t, "一号窗口").start(); new Thread(t, "二号窗口").start(); new Thread(t, "三号窗口").start(); new Thread(t, "四号窗口").start(); }}
加入同步监视器之后的程序就不会出现数据上的错误了。
虽然同步监视器的好处是解决了多线程的安全问题。但也也因为多个线程需要判断锁,较为消耗资源。
同步前提:
- 必须要有两个或者两个以上的线程,才需要同步。
- 多个线程必须保证使用的是同一个锁。
如果加入了synchronized 同步监视器,还出现了安全问题,则可以按照如下步骤找寻错误:
- 明确那些代码是多线程代码。
- 明确共享数据。
- 明确多线程运行代码中那些代码是操作共享数据的。
同步函数
把 synchronized 作为修饰符修饰函数。则该函数称为同步函数。
注意:同步函数无需显示的指定同步监视器,函数都有自己所属的对象this,同步函数的同步监视器是this,也就是该对象本身。
注意:synchronized 关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等。
上面通过模拟火车卖票系统的小程序,通过加入 synchronized 同步监视器,来解决多线程中的安全问题。下面模拟银行取钱问题,通过同步函数来解决多线程的安全问题。
package heimablog;class Account { // 账户余额 private double balance; public Account(double balance) { this.balance = balance; } // get和set方法 public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } // 同步函数 // 提供一个线程安全的draw的方法来完成取钱操作。 public synchronized void Draw(double drawAmount) { if (balance >= drawAmount) { System.out.println(Thread.currentThread().getName() + "取钱成功!吐出金额" + drawAmount); try { Thread.sleep(10); } catch (Exception e) { } balance -= drawAmount; System.out.println("卡上余额:" + balance); } else { System.out.println(Thread.currentThread().getName() + "取钱失败!卡上余额:" + balance); } }}class DrawThread implements Runnable { // 模拟账户 private Account account; // 希望所取钱的金额 private double drawAmount; private boolean flag = true; public DrawThread(Account account, double drawAmount) { this.account = account; this.drawAmount = drawAmount; } // 当前取钱 public void run() { try { while (flag) { // 调用取钱函数 account.Draw(drawAmount); } } catch (Exception e) { flag = false; } }}public class ShowTest { public static void main(String args[]) { Account account = new Account(10000); DrawThread draw = new DrawThread(account, 50); Thread t = new Thread(draw, "A....."); Thread t1 = new Thread(draw, "B."); t.start(); t1.start(); }}
同步方法的监视器是 this ,因此对于同一个 Account 而言,任意时刻只能有一条线程获得 Account 对象的锁定。
提示:可变类的线程安全是以降低运行程序的运行效率作为代价,为了减少线程安全所带来的负面影响,程序可以采用如下策略:
- 只对会改变竞争资源的方法进行同步。
- 在两个或两个以上的线程操作同一个锁的环境中使用同步。
当如下情况发生时会释放同步监视器的锁定:
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程的同步方法、同步代码块中遇到break 、 return终止了该代码块、该方法的继续执行。
- 当前线程的同步方法、同步代码块出现了未处理的Error或Exception,导致该代码块、该方法异常结束时会释放同步锁。
- 当线程执行同步方法、同步代码块,程序执行了同步监视器对象的wait() 方法时。
当同步函数被static修饰时,这时的同步用的是哪个锁呢?
静态函数在加载时所属于类,这时有可能还没有该类产生的对象,但是该类的字节码文件加载进内存就已经被封装成了对象,这个对象就是该类的字节码文件对象。
所以静态加载时,只有一个对象存在,那么静态同步函数就使用的这个对象。这个对象就是 类名.class
同步代码块和同步函数的区别?
- 同步代码块使用的锁可以是任意对象。
- 同步函数使用的锁是this,静态同步函数的锁是该类的字节码文件对象。
在一个类中只有一个同步,可以使用同步函数。如果有多同步,必须使用同步代码块,来确定不同的锁。所以同步代码块相对灵活一些。
死锁
当两线程相互等待对方释放锁时,就会发生死锁。由于JVM没有监测,也没有采用措施来处理死锁,所以多线程编成时应该采取措施来避免死锁。
单例模式之懒汉式
懒汉式:延迟加载方式。
当多线程访问懒汉式时,因为懒汉式的方法内对共性数据进行多条语句的操作,所以容易出现线程安全问题。
- 为了解决安全问题,加入同步机制。但是却带来了效率降低。
- 为了解决效率问题,通过双重判断的形式解决。
class Single { private static Single s = null; private Single() {
} public static Single getInstance() { if (s == null) { synchronized (Single.class) {//用字节码文件对象作为锁; if (s == null) s = new Single(); } } return s; }}
同步死锁:通常只要将同步进行嵌套,就可以看到现象。
线程通信
思路:多个线程在操作同一个资源,但是操作的动作却不一样。
- 将资源封装成对象。
- 将线程执行的任务(任务其实就是run方法。)也封装成对象。
模拟生产消费者:系统在有两条线程,分别代表生成者和消费者。
程序的基本流程:
- 生成者生产出一件商品。
- 消费者消费生成出的商品。
通过上诉流程要了解:生成者和消费者不能连续生成或消费商品,同时消费者只能消费已经生产出的商品。
package heimablog;class Phone { // 定义商品的编号 private int No; // 定义商品的名字 private String name; private boolean flag = true; // 初始化商品的名字和编号,同时编号是自增的 public Phone(String name, int No) { this.name = name; this.No = No; } // 定义商品中的消费方法和生产方法。用synchronized 修饰符修饰 public synchronized void Production() { // 导致当前线程等待,知道其他线程调用notify()或notifyAll()方法来唤醒 // if (!flag) while (!flag) try { this.wait(); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() + ":生产" + name + ";编号为:" + ++No); // 唤醒在此同步监视器上等待的单个线程。 // this.notify() ; // 唤醒在此同步监视器上等待的所有线程。 this.notifyAll(); flag = false; } public synchronized void Consumption() { // if(flag) while (flag) try {this.wait();} catch (Exception e) {} System.out.println(Thread.currentThread().getName() + ";消费商品:" + name + "商品的编号为" + No); // this.notify() ; this.notifyAll(); flag = true; }}class ProducerThread implements Runnable { Phone phone; private boolean flag = true; // 同步监视器的对象 public ProducerThread(Phone phone) { this.phone = phone; } public void run() { try { while (flag) phone.Production(); } catch (Exception e) { flag = false; } }}class ConsumptionThread implements Runnable { Phone phone; private boolean flag = true; // 同步监视器的对象 public ConsumptionThread(Phone phone) { this.phone = phone; } public void run() { try { while (flag) phone.Consumption(); } catch (Exception e) { flag = false; } }}public class ShowTest { public static void main(String args[]) { Phone phone = new Phone("iPhone 5", 0); new Thread(new ProducerThread(phone), "生成者000").start(); new Thread(new ProducerThread(phone), "生成者111").start(); new Thread(new ConsumptionThread(phone), "消费者000").start(); new Thread(new ConsumptionThread(phone), "消费者111").start(); }}
上面的程序中:flag 标志位 是判断 是由生产者生成还是由消费者进行消费。其实,在现实生活中,不可能只有一个生成者和消费者,而是多个生成者和消费者。所以用 while 循环来进行 flag 的判断 , 而不是用 if 。如果用if 容易出现线程安全问题;而且在用while 循环进行flag的判断时,则必须用 notifyAll() 方法来唤醒同步监视器中所有等待中的线程,而不是 用notify() 方法。用notify() 则会导致所有线程进入等待状态。
上面的小程序借助Object 类提供的 wait()、notify()、notifyAll 三个方法【等待唤醒机制涉及的方法】
- wait() :导致当前线程等待,知道其他线程调用该同步监视器的notify()或notifyAll() 方法来唤醒线程。
- notify() : 唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择一个其中一个唤醒。
- notifyAll() :唤醒此同步监视器上等待的所有单个线程。
注意:
- 这三个方法必须用同步监视器对象来调用:
- 同步函数:因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用。
- 同步代码块:必须使用 synchronized 括号中的对象来调用。
- 这些方法都需要定义在同步中:因为这些方法必须要标示所属的锁。你要知道 A锁上的线程被wait了,那这个线程就相当于处于A锁的线程池中,只能A锁的notify唤醒。
- 这三个方法都定义在Object类中。为什么操作线程的方法定义在Object类中?因为这三个方法都需要定义同步内,并标示所属的同步锁,既然被锁调用,而锁又可以是任意对象,那么能被任意对象调用的方法一定定义在Object类中。
wait和sleep区别
分析这两个方法从执行权和锁上来分析:
wait:可以指定时间也可以不指定时间。不指定时间,只能由对应的notify或者notifyAll来唤醒。
sleep:必须指定时间,时间到自动从冻结状态转成运行状态(临时阻塞状态)。
wait:线程会释放执行权,而且线程会释放锁。
Sleep:线程会释放执行权,但不是不释放锁。
线程的停止【stop方法已过时】
原理:让线程运行的代码结束,也就是结束run方法。
怎么结束run方法?一般run方法里肯定定义循环。所以只要结束循环即可。
- 第一种方式:定义循环的结束标记。
- 第二种方式:如果线程处于了冻结状态,是不可能读到标记的,这时就需要通过Thread类中的interrupt方法,将其冻结状态强制清除。让线程恢复具备执行资格的状态,让线程可以读到标记,并结束。
Thread‘s functions
- interrupt():中断线程。
- setPriority(int newPriority):更改线程的优先级。
- getPriority():返回线程的优先级。
- toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
- Thread.yield():暂停当前正在执行的线程对象,并执行其他线程。
- setDaemon(true):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
- join:临时加入一个线程的时候可以使用join方法。当A线程执行到了B线程的join方式。A线程处于冻结状态,释放了执行权,B开始执行。A什么时候执行呢?只有当B线程运行结束后,A才从冻结状态恢复运行状态执行。
同步锁LOCK
JDK 1.5之后,JAVA提供了另外一种线程同步机制:显示定义同步锁来实现同步,解决线程安全问题使用同步的形式,(同步代码块,要么同步函数)其实最终使用的都是锁机制。同步锁应该使用Lock对象充当。在面向对象中谁拥有数据谁就对外提供操作这些数据的方法 ,发现获取锁,或者释放锁的动作应该是锁这个事物更清楚。所以将这些动作定义在了锁当中,并把锁定义成对象。线程进入同步就是具备了锁,执行完,离开同步,就是释放了锁。
class X { // 定义锁对象 private final ReentrantLock lock = new ReentrantLock(); // 定义需要保证线程安全的方法 public void m() { // 加锁 lock.lock(); try { // 需要保证线程安全的代码 } finally { lock.unlock(); } }}
所以同步是隐示的锁操作,而Lock对象是显示的锁操作,它的出现就替代了同步。
现在锁是指定对象Lock。所以查找等待唤醒机制方式需要通过Lock接口来完成。而Lock接口中并没有直接操作等待唤醒的方法,而是将这些方式又单独封装到了一个对象中。这个对象就是Condition,将Object中的三个方法进行单独的封装,并提供了功能一致的方法 await()、signal()、signalAll().
Condition接口:await()、signal()、signalAll();
Condition 实例实质上被绑定在一个Lock 对象上。如:
//定义锁对象private final ReentrantLock lock = new ReentrantLock() ; //指定Lock 对象对应的条件变量private final Condition condition = lock.newCondition() ;
- await() : 类似 wait() 方法。
- signal() : 类似 notify() 方法。
- signalAll() : 类似 notifyAll() 方法。