JAVA学习笔记————多线程
1. 多线程
线程是在进程的基础上进一步划分的,所以,如果进程消失了,则线程肯定消失,而如果线程消失的话进程可能依然会继续。Java属于多线程的操作语言,所以提供了线程的处理机制。在Java中可以有两种方式实现多线程操作,一种是继承Thread类,另一种是实现Runable接口。
1.1 线程实现的两种手段 1<Thread 类>
Thread 类是在java.lang一个包中定义的。
一个类只要是继承了Thread类,同时覆写了本类中的run()方法,则就可以实现多线程的操作了。
package com.java.threadDemo; public class MyThread extends Thread { private String name; public MyThread(String name) { this.name = name; } public void run(){ for (int i = 0; i < 5; i++){ System.out.println("Thread 运行:" + name + "\ti= " + i); } } } |
以上的类已经完成了多线程的操作类,那么就启动线程
package com.java.threadDemo; public class ThreadDemo01 { public static void main(String[] args) { MyThread myThreadA = new MyThread("线程 A"); MyThread myThreadB = new MyThread("线程 B"); myThreadA.run(); // 调用线程体 myThreadB.run(); // 调用线程体 } } |
但是此时的执行结构发现非常的有规律,先执行完第一个对象,在执行第二个对象,也就是说并没有实现交互的运行。
从JDK文档中发现,一旦调用start()方法,则会通过JVM找到run()方法。public void start()
那么现在使用start()启动线程:
package com.java.threadDemo; public class ThreadDemo02 { public static void main(String[] args) { MyThread myThreadA = new MyThread("线程 A"); MyThread myThreadB = new MyThread("线程 B"); myThreadA.start(); // 调用线程体 myThreadB.start(); // 调用线程体 } } |
观察发现,此时程序已经可以正常的进行交互式的运行了。但是,需要思考的是,为什么非要使用start()方法启动多线程呢???
在JDK的安装目录,找到start()的定义
public synchronized void start() { if (threadStatus != 0) // 判断线程是否启动 throw new IllegalThreadStateException(); // 如果已经启动,则抛出异常 group.add(this); boolean started = false; try { start0(); // 调用start0()方法 started = true; // 如果没有,则修改状态 } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } private native void start0(); // 使用native 关键字声明,没有方法体。 |
操作系统有很多种,既然在多线程的操作中要进行CPU资源的抢占,也就是说要等待CPU的调度,那么这些调度的操作是由各个操作系统的底层实现的,所以在Java程序中根本就没有办法实现,那么此时Java的设计者定义了native关键字,使用此关键字表示可以调用操作系统的底层函数,这样的技术有称为JNI(Java Native Interface)[1]。而且此方法在执行的时候将调用run()方法完成,由系统默认调用。
这就是第一种实现多线程的手段,但是第一种方法有一个最大的限制,那就是一个类只能继承一个父类,那么现在既要实现多线程,又要继承其他父类,又该真么办呢???
1.2 线程实现的两种手段 2<Runnable 接口>
在实际开发中一个多线程的操作类很少会使用Thread类完成,而是通过Runnable接口完成。观察Runnable的接口定义:
public interface Runnable{ public void run(); } |
所以说一个类只要实现了此接口,并覆写了此方法。
package com.java.runableDemo; public class MyThread implements Runnable{ private String name; public MyThread(String name) { this.name = name; } public void run(){ for (int i = 0; i < 10; i++){ System.out.println("Thread 运行:" + name + "\ti = " + i); } } } |
完成之后,下面继续启动多线程。单丝现在使用Runnable定义的子类中并没有start()方法,只有Thread类中才有。
在Thread类中存在以下的一个构造方法
public Thread(Runnable target) |
此构造方法接收Runnable的子类实例,也就是说现在可以通过Thread类来启动Runnable直线多线程
package com.java.runableDemo; public class MyThread implements Runnable{ private String name; public MyThread(String name) { this.name = name; } public void run(){ for (int i = 0; i < 50; i++){ System.out.println("Thread 运行:" + name + "\ti = " + i); } } } |
以上的程序代码页是交替运行的,所以此时程序也同样实现了多线程的操作。
1.3 两种实现方式的区别及联系
在程序的开发中只要是多线程,则肯定以实现Runnable接口为正统操作,因为Runnable接口相比继承Thread类有如下好处:
·避免单继承的局限,一个类可以实现多个接口。
·是何意资源的共享(重要)
下面以买票的程序为例,通过Thread类完成。
package com.java.threadDemo01; public class MyThread extends Thread { private int ticket = 5; // 一共有5张票 public void run() { for (int i = 0; i < 10; i++) { if (this.ticket > 0) { System.out.println("卖票:ticket = " + this.ticket--); } } } } |
现在建立三个线程对象,同时卖票。
package com.java.threadDemo01; public class TicketThread { public static void main(String[] args) { MyThread myThreadA = new MyThread(); MyThread myThreadB = new MyThread(); MyThread myThreadC = new MyThread(); myThreadA.start(); myThreadB.start(); myThreadC.start(); } } |
发现现在一共买了 15 张票,但是实际上只有 5 张票。所以证明每一个线程都是在卖自己的票,并没有达到资源共享的目的。
那么如果现在,实现的是Runnable接口的话,则就可以实现资源的共享:
package com.java.diffDemo02; public class MyThread implements Runnable { private int ticket = 5; // 一共有5张票 public void run() { for (int i = 0; i < 10; i++) { if (this.ticket > 0) { System.out.println("卖票:ticket = " + this.ticket--); } } } } |
下面编写多个线程进行卖票。
package com.java.diffDemo02; public class TicketThread { public static void main(String[] args) { MyThread myThread = new MyThread(); new Thread(myThread).start(); new Thread(myThread).start(); new Thread(myThread).start(); } } |
现在虽然程序中有三个线程,但是三个线程一共才卖出了 5 张票,也即是说使用Runnable实现的多线程可以达到资源共享的目的。
实际上Runnable接口和Thread类还是存在联系的。
public class Thread extends Object implements Runnable |
1.4 线程的操作方法
1.4.1 设置和取得方法
在Thread类中存在以下几个方法,可以设置和取得名字:
·设置名字:
·public final void setName(String name)
·public Thread(Runnable target, String name)
·public Thread(String name)
·取得名字:
·public final String getName()
·在线程的操作中因为其操作的不确定性,所以提供了一个方法,可以取得当前的操作线程:
·public static Thread currentThread()
对于线程的名字,一般是在启动前进行设置,最好不要设置相同的名字,最好不要为一个线程更改名字。
package com.java.nameDemo; public class MyThread implements Runnable { public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " 线程正在运行。"); } } } |
那么此时测试一下上面却线程的名字
package com.java.nameDemo; public class ThreadNameDemo { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread threadA = new Thread(myThread, "线程A"); Thread threadB = new Thread(myThread, "线程B"); Thread threadC = new Thread(myThread, "线程C"); threadA.start(); threadB.start(); threadC.start(); } } |
明白以上代码的作用之后,再来看一下以下代码:
package com.java.nameDemo; public class ThreadNameDemo02 { public static void main(String[] args) { MyThread myThread = new MyThread(); new Thread(myThread, "线程A").start(); myThread.run(); } } |
在输出结果中发现有main 线程正在运行。 所以得出一个结论,在程序运行是,主方法实际上就是一个主线程。一直强调,Java是一个多线程的操作语言,那么它是怎么实现多线程的呢???
实际上对于Java来讲,每一次执行java命令对于操作系统来说豆浆启动一个JVM的进程,那么主方法实际上就是这个进程上的进一步划分。
问题:在java执行中,一个java程序至少会启动几个线程???
至少会启动两个:main(主线程)、gc(垃圾回收)
1.4.2 线程的休眠
让程序小小的休息一下,然后再继续工作:
·public static void sleep(long millis) throws InterruptedException
在使用此方法是,需要进行try…catch处理操作。
package com.java.sleepDemo; public class MyThread implements Runnable { public void run() { for (int i = 0; i < 10; i++) { try { Thread.sleep(500); // 每次休眠500毫秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 线程正在运行。"); } } } |
主程序:
package com.java.sleepDemo; public class ThreadSleepDemo01 { public static void main(String[] args) { MyThread myThread = new MyThread(); new Thread(myThread,"线程A").start(); new Thread(myThread,"线程B").start(); } } |
发现程序的执行变得缓慢了,这就是线程的休眠操作。
1.4.3 线程的中断
在sleep()方法中存在InterruptedException,那么会造成此一场的方法就是中断:
·public void interrupt()
package com.java.interruptDemo; public class MyThread implements Runnable { public void run() { System.out.println("1.正常进入run()方法体"); try { System.out.println("2.线程休眠5秒"); Thread.sleep(5000); // 每次休眠5秒 System.out.println("3.线程休眠结束"); } catch (InterruptedException e) { System.out.println("4.线程休眠被中断"); return; } System.out.println("5.正常结束run()方法体"); } } |
package com.java.interruptDemo; public class InterruptDemo01 { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread threadA = new Thread(myThread, "线程A"); threadA.start(); try { Thread.sleep(2000); // 保证程序至少执行两秒 } catch (InterruptedException e) { e.printStackTrace(); } threadA.interrupt(); } } |
1.4.4 线程的优先级
在线程的操作中,左右的线程都是有优先级的,优先级高的就有可能先执行,在操作中使用以下方法进行优先级的设置:public final void setPriority(int newPriority)
线程中的优先级有以下三种:
·最高:public static final int MAX_PRIORITY
·最低:public static final int MIN_PRIORITY
·中等(默认):public static final int NORM_PRIORITY
package com.java.priorityDemo01; public class MyThread implements Runnable { public void run() { for (int i = 0; i < 10; i++) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 线程正在运行。"); } } } |
主方法:
package com.java.priorityDemo01; public class PriorityDemo01 { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread threadA = new Thread(myThread, "线程A"); Thread threadB = new Thread(myThread, "线程B"); Thread threadC = new Thread(myThread, "线程C"); threadA.setPriority(Thread.NORM_PRIORITY); threadB.setPriority(Thread.MIN_PRIORITY); threadC.setPriority(Thread.MAX_PRIORITY); threadA.start(); threadB.start(); threadC.start(); } } |
每个线程都可以设置优先级,那么主线程的优先级是什么呢???
package com.java.priorityDemo01; public class PriorityDemo02 { public static void main(String[] args) { System.out.println("主线程 的优先级:" + Thread.currentThread().getPriority()); System.out.println("Thread.MAX_PRIORITY 的优先级:" + Thread.MAX_PRIORITY); System.out.println("Thread.MIN_PRIORITY 的优先级:" + Thread.MIN_PRIORITY); System.out.println("Thread.NORM_PRIORITY 的优先级:" + Thread.NORM_PRIORITY); } } |
输出结果:
主线程 的优先级:5 Thread.MAX_PRIORITY 的优先级:10 Thread.MIN_PRIORITY 的优先级:1 Thread.NORM_PRIORITY 的优先级:5 |
从输出结果来看,主方法的优先级是普通的,而且优先级越高,数值越大。
1.5 同步与死锁(理解)
1.5.1 问题的引出
现在有如下代码:
package com.java.synDemo; public class MyThread implements Runnable { private int ticket = 5; // 一共有5张票 public void run() { for (int i = 0; i < 50; i++) { if (this.ticket > 0) { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("卖票:ticket = " + this.ticket--); } } } } |
package com.java.synDemo; public class synDemo01 { public static void main(String[] args) { MyThread myThread = new MyThread(); new Thread(myThread, "售票窗口A:").start(); new Thread(myThread, "售票窗口B:").start(); new Thread(myThread, "售票窗口C:").start(); } } |
运行发现卖出的票会出现0 甚至为负,显然是不正确的。照成此类问题的根本原因在于,判断剩余票数和修改票数之间加入了延时操作(实际生活中肯定会有网络延时的问题)。
如果想要解决这样的问题,就必须使用同步,所谓的同步就是指多个操作在同一个时间段内只能有一个线程在进行,其他的线程需要等到此线程完成之后才能继续执行。
1.5.2 同步解决
在Java中可以通过同步代码的方式进行代码的枷锁操作,同步的实现由两种方式:
·同步代码块 ·同步方法
同步代码块:
使用synchronized关键字进行同步代码块的声明,但是使用此操作的时候必须要明确的指出到底要锁定的是哪个对象,一般都是以当前对象为主。
synchronized(对象){ //一般都是将this进行锁定 需要同步的代码; } |
使用同步代码块修改之前的程序:
package com.java.synDemo02; public class MyThread implements Runnable { private int ticket = 5; // 一共有5张票 public void run() { for (int i = 0; i < 50; i++) { synchronized (this) { // 形成同步代码块 if (this.ticket > 0) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("卖票:ticket = " + this.ticket--); } } } } } |
主方法:
package com.java.synDemo02; public class synDemo01 { public static void main(String[] args) { MyThread myThread = new MyThread(); new Thread(myThread, "售票窗口A:").start(); new Thread(myThread, "售票窗口B:").start(); new Thread(myThread, "售票窗口C:").start(); } } |
此时却是解决了同步的问题,但是在Java中呢也可以通过同步方法来解决这样的问题
同步方法:
package com.java.synDemo03; public class MyThread implements Runnable { private int ticket = 5; // 一共有5张票 public void run() { for (int i = 0; i < 50; i++) { this.sale(); } } public synchronized void sale() { // 增加同步方法 if (this.ticket > 0) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("卖票:ticket = " + this.ticket--); } } } |
package com.java.synDemo03; public class synDemo01 { public static void main(String[] args) { MyThread myThread = new MyThread(); new Thread(myThread, "售票窗口A:").start(); new Thread(myThread, "售票窗口B:").start(); new Thread(myThread, "售票窗口C:").start(); } } |
此时就可以给出Java中方法定义的完整格式的
[访问权限][static][final][abstract][synchronized][native][返回值类型][方法名称([参数列表])[throws][异常 1,异常2,…]{} |
发现同步可以保证资源的完整性,但是过多的同步也会出现问题:
1.5.3 死锁
在程序中,过多的同步会产生死锁问题,那么死锁属于程序运行的时候发生的一种特殊状态,本章知识延时一个简单的操作代码,只要观察到死锁最终的运行状态即可。
package com.java.deadLockDemo; public class DeadLockDemo01 implements Runnable { private A a = new A(); private B b = new B(); public DeadLockDemo01() { new Thread(this).start(); a.say(b); } public void run() { b.say(a); } public static void main(String[] args) { new DeadLockDemo01(); } } class A { public synchronized void say(B b) { System.out.println("A: 把钱给我,放了你儿子。。。"); b.give(); } public synchronized void give() { System.out.println("得到了前,被警察抓了。。。"); } } class B { public synchronized void say(A a) { System.out.println("B: 把我儿子放了,给你钱。。。"); a.give(); } public synchronized void give() { System.out.println("儿子救回来了,报警了。。。"); } } |
在本块中只需要记住一个概念即可:多个线程共享同一资源的时候需要进行同步,但是过多的同步会造成死锁。
1.6 生产者与消费者(理解)
在多线程中有一个最经典的操作案例——生产者和消费者,生产者不断产生内容,但是消费者不断取出内容
1.6.1 基本实现
现在假设生产的内容都保护在Info类中,则生产者要有Info类的引用,而消费者也要存在Info类的引用。生产者应该不断的生产信息,消费者不断的取出,所以事先多线程的操作,从代码中可以发现以下几点问题:
·生产的内容可能出现不一致的情况 ·出现了重复取值和重复设置的问题
package com.java.pcdemo01; public class Info { private String title = null; private String content = null; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } |
package com.java.pcdemo01; public class Consumer implements Runnable { private Info info = null; public Consumer(Info info) { this.info = info; } public void run() { for (int x = 0; x < 50; x++) { try { Thread.sleep(300); // 休眠0.3秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.info.getTitle() + " -> " + this.info.getContent()); // 取内容 } } } |
package com.java.pcdemo01; public class Producer implements Runnable { private Info info = null; public Producer(Info info) { this.info = info; } public void run() { for (int x = 0; x < 50; x++) { // 不断的生产 if (x % 2 == 1) {// 是奇数 this.info.setTitle("hao 123"); try { Thread.sleep(300); // 休眠0.3秒 } catch (InterruptedException e) { e.printStackTrace(); } this.info.setContent("www.hao123.com"); } else { this.info.setTitle("百度"); try { Thread.sleep(300); // 休眠0.3秒 } catch (InterruptedException e) { e.printStackTrace(); } this.info.setContent("www.baidu.com"); } } } } |
package com.java.pcdemo01; public class TestInfoDemo01 { public static void main(String[] args) { Info info = new Info(); Producer pro = new Producer(info); // 实例化生产者对象 Consumer con = new Consumer(info); // 实例化消费者对象 new Thread(pro).start(); // 启动线程 new Thread(con).start(); // 启动线程 } } |
1.6.2 加入同步
为了保证程序操作数据的完整性,此时,最好加入同步操作,可以直接在Info类中定义一个同步方法,专门完成设置和取得的内容。
package com.java.pcdemo02; public class Info { private String title = null; private String content = null; public synchronized void set(String title, String content) { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } this.setTitle(title); this.setContent(content); } public synchronized void get() { System.out.println(this.title + " --> " + this.content); } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } |
package com.java.pcdemo02; public class Producer implements Runnable { private Info info = null; public Producer(Info info) { this.info = info; } public void run() { for (int x = 0; x < 50; x++) { // 不断的生产 if (x % 2 == 1) {// 是奇数 this.info.set("百度", "www.baidu.com"); } else { this.info.set("hao 123", "www.hao123.com"); } } } } |
package com.java.pcdemo02; public class Consumer implements Runnable { private Info info = null; public Consumer(Info info) { this.info = info; } public void run() { for (int x = 0; x < 50; x++) { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } this.info.get(); } } } |
package com.java.pcdemo02; public class TestInfoDemo02 { public static void main(String[] args) { Info info = new Info(); Producer pro = new Producer(info); // 实例化生产者对象 Consumer con = new Consumer(info); // 实例化消费者对象 new Thread(pro).start(); // 启动线程 new Thread(con).start(); // 启动线程 } } |
此时可以保证数据的一致性,但是依然存在重复和重复设置的问题,那么该如何去掉这些问题呢???
1.6.3 Object对线程的支持
对于唤醒的操纵有两个:notify()、notifyAll()。一般来说,所有等到的线程会按照顺序进行排列,如果现在使用notify()方法的话,则会唤醒第一个等待的线程执行,而如果使用了notifyAll()方法,则会唤醒所有的等待线程,哪个线程的优先级高,哪个线程就有可能优先执行。
现在就利用Object类中的以上方法来解决线程的等待和唤醒操作。修改Info类
package com.java.pcdemo03; public class Info { private String title = "百度"; private String content = "www.baidu.com"; /** * flag = false 表示可以生产,但是不能取走 * flag = true 表示可以取走,但是不能生产 */ private boolean flag = false; public synchronized void set(String title, String content) { if(flag == false){ try { super.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this.setTitle(title); try { Thread.sleep(300); // 休眠0.3秒 } catch (InterruptedException e) { e.printStackTrace(); } this.setContent(content); this.flag = false; // 表示不能生产了 super.notify(); } public synchronized void get() { if(flag == true){ try { super.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(this.title + " --> " + this.content); this.flag = true; // 表示不能取出了 super.notify(); // 唤醒其他线程 } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } |
package com.java.pcdemo03; public class Producer implements Runnable { private Info info = null; public Producer(Info info) { this.info = info; } public void run() { for (int x = 0; x < 50; x++) { // 不断的生产 if (x % 2 == 1) {// 是奇数 this.info.set("百度", "www.baidu.com"); } else { this.info.set("hao 123", "www.hao123.com"); } } } } |
package com.java.pcdemo03; public class Consumer implements Runnable { private Info info = null; public Consumer(Info info) { this.info = info; } public void run() { for (int x = 0; x < 50; x++) { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } this.info.get(); } } } |
package com.java.pcdemo03; public class TestInfoDemo03 { public static void main(String[] args) { Info info = new Info(); Producer pro = new Producer(info); // 实例化生产者对象 Consumer con = new Consumer(info); // 实例化消费者对象 new Thread(pro).start(); // 启动线程 new Thread(con).start(); // 启动线程 } } |