一、概述
线程的前提是有进程,所以说线程之前的了解进程的概念及其与线程的联系。
进程:是一个正在执行中的程序。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。
线程:是进程中的一个独立的控制单元,线程在控制着进程的执行。一个进程中至少有一个线程,每个独立线程代表一个独立操作。线程隶属于某个进程,它自身没有入口和出口;也不能自动运行,要由进程启动执行,进行控制。
两者的区别:
1)进程在执行过程中拥有独立的内存单元,而多个线程共享内存。
2)线程都有自己默认的名称。Thread-编号,该编号从 0 开始。
多线程:一个进程中有多个线程,称为多线程。例如:虚拟机启动的时候就是多线程,JVM 启动时至少有一个主线程和一个负责垃圾回收的线程。
主线程:在 JVM 启动时会有一个进程 Java.exe。该进程中至少一个线程负责 Java程序的执行,而这个线程运行的代码存在于main 方法中,则该线程称为主线程。
多线程的意义:多个程序同时执行,从而提高程序运行效率。
线程的弊端:线程太多会导致效率的降低,因为线程的执行依靠的是 CPU 的来回切换。
多线程原理:
当运行多线程程序时发现运行结果每次都不同。因为多个线程都在获取CPU的执行权。CPU执行到谁,谁就运行。明确一点,在某一时刻,只能有一个程序在运行。(多核除外)CPU在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象把多线程运行行为看做在互相抢夺CPU的执行权。这就是多线程的一个特性,随机性。谁抢到谁执行,至于执行多长时间,CPU说了算。后期可以控制,但是比较困难。系统可以同时执行多个任务,应用程序内的多任务并行就是依靠多线程实现的。
二、线程创建
创建线程有两种方式:继承Thread类、实现Runnable接口。
1、继承Thread类:
步骤:
1)定义类继承 Thread。
2)复写 Thread 中的 run()方法(将线程要运行的代码存放在该 run()方法中) 。
3)调用线程的 start()方法启动线程,从而调用 run()方法。
Note:
Thread 类用于描述线程。该类定义了一个功能,用于存储线程要运行的代码。该存储功能就是 run()方法。因此,run()方法的目的就是将自定义的代码存储在 run()方法,让线程运行。在 main 方法中调用 start()方法作用是开启线程并执行线程的 run ()方法。而在 main 方法中直接调用 run ()方法,仅仅是对象调用方法,创建了线程,并没有运行。
2、实现Runnable接口:
步骤:
1)定义类实现 Runnable 接口。
2)覆盖 Runnable 接口中的 run()方法(将线程要运行的代码存放在该 run()方法中 )。
3)通过 Thread 类建立线程对象。
4)将 Runnable 接口的子类对象作为实际参数传递给 Thread 类的构造函数。
5)调用 Thread 类的 start 方法开启线程从而调用 Runnable 接口子类的 run()方法。
Note:
创建(声明)一个实现 Runnable 接口的类对象。类必须定义一个称为 run 的无参方法。此外 Runnable 为 Thread 的子类的类提供了一种激活方式。然后该类实现 run()方法,可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。
下面演示两种方式创建线程:
class Demo extends Thread {
public void run() {
for (int i = 0; i < 60; i++) {
System.out.println(Thread.currentThread().getName() + "::Demo run ---");
}
}
}
class Demo1 implements Runnable {
public void run() {
for (int i = 0; i < 60; i++) {
System.out.println(Thread.currentThread().getName() + "::Demo1 run ---");
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Demo d = new Demo();
d.start();
Demo1 d1 = new Demo1();
new Thread(d1).start();;
for (int i = 0; i < 60; i++) {
System.out.println(Thread.currentThread().getName() + "::Main run ---");
}
}
}
结果如下所示:
3、两种创建方式的区别
继承Thread:线程代码存放在Thread子类run()方法中。
实现Runnable:线程代码存放在接口子类run()方法中。
实现方式的好处:避免了单继承的局限性。定义线程时,建议使用实现方式。
4、线程运行状态
被创建:等待启动,调用start启动。
运行状态:具有执行资格和执行权。
临时状态(阻塞):有执行资格,但是没有执行权。
冻结状态:遇到sleep(time)方法和wait()方法时,失去执行资格和执行权,sleep()方法时间到或者调用notify()方法时,获得执行资格,变为临时状态。
消忙状态:stop()方法,或者run()方法结束。
图解如下:
三、线程同步机制
1、多线程的安全问题原因:当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完, 另一个线程参与进来执行。导致共享数据的错误。
2、解决办法:对多条操作共享数据的语句,只能让一个线程执行完。在执行过程中,其他线程不可以参与执行———同步。
Note:
同步的前提:必须要有两个或者两个以上的线程,必须是多个线程使用同一个锁,必须保证同步中只能有一个线程在运行。
同步优点:解决了多线程的安全问题。
同步弊端:多个线程需要判断锁,较为消耗资源。
在多线程操作共享数据的运行代码中,需要加锁的两种情况:
1)含有选择判断语句
2)含 try(){}catch(){}语句
理解:对象如同锁,持有锁的线程可以在同步中执行。没有持有锁的线程即使获取CPU 执行权,也进不去,因为没有获取锁。Java 对于多线程的安全问题提供了专业的解决方式:同步代码块、同步函数。
3、同步代码块:
格式:
synchronized(对象)
{
//需要被同步的代码
}
示例:
/*
* 创建4个线程同时卖100张票
*
*/
class Ticket implements Runnable {
private int num = 100;
public void run() {
while (true) {
//同步代码块,利用this作为锁,或者用Class对象作为锁,只要保证锁唯一即可
synchronized (this) {
if (num > 0) {
try {
//延时是为了让4个线程执行权趋于均衡
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Ticket" + num--);
}
}
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
Thread t4 = new Thread(ticket);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
4、同步函数
格式:在函数上加上synchronized修饰符即可。
Note:那么同步函数用的是哪一个锁呢?
函数需要被对象调用。那么函数都有一个所属对象引用,就是this,所以同步函数使用的锁是this。
示例:
/*
* 创建4个线程同时卖100张票
*
*/
class Ticket implements Runnable {
private int num = 100;
public synchronized void run() {
while (true) {
// 同步代码块,利用this作为锁,或者用Class对象作为锁,只要保证锁唯一即可
// synchronized (this) {
if (num > 0) {
try {
// 延时是为了让4个线程执行权趋于均衡
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Ticket" + num--);
}
// }
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
Thread t4 = new Thread(ticket);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
上述两个示例结果都会随着运行而改变,但是基本完成100张车票的售卖,结果如下:
5、静态函数的同步
如果同步函数被静态修饰后,使用的锁是什么呢?
通过验证,发现不在是this。因为静态方法中也不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。如:类名.class 该对象的类型是Class这就是静态函数所使用的锁。而静态的同步方法,使用的锁是该方法所在类的字节码文件对象——类名.class
示例:
class Single {
private static Single s = null;
private Single() {
}
public static Single getInstance() {
//解决效率问题
if (s == null) {
//加锁
synchronized (Single.class) {
//判断s是否有非空指向
if (s == null) {
s = new Single();
}
}
}
return s;
}
}
6、死锁多线程程序中,当同步中嵌套同步时,就有可能出现死锁现象。
示例:
class DeadLock implements Runnable {
private boolean flag;
DeadLock(boolean flag) {
this.flag = flag;
}
public void run() {
if (flag) {
synchronized (MyLock.locka) {
System.out.println("...if locka");
synchronized (MyLock.lockb) {
System.out.println("...else lockb");
}
}
} else {
synchronized (MyLock.lockb) {
System.out.println("...else lockb");
synchronized (MyLock.locka) {
System.out.println("...else locka");
}
}
}
}
}
//自定义锁类
class MyLock {
static MyLock locka = new MyLock();
static MyLock lockb = new MyLock();
}
public class ThreadDemo7 {
public static void main(String[] args) {
DeadLock dlt = new DeadLock(true);
DeadLock dlf = new DeadLock(false);
Thread t1 = new Thread(dlt);
Thread t2 = new Thread(dlf);
t1.start();
t2.start();
}
}
结果如下所示:
四、线程间通信
其实就是多个线程在操作同一个资源,但是操作的动作不同。
示例:
/*
* 练习:一个线程存取姓名和性别,另一个线程打印出该两项
*
*/
//资源
class Res {
String name;
String sex;
boolean flag;//判断资源是否存在的标识符
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
//存放线程
class Input implements Runnable {
Res r = new Res();
Input(Res r) {
this.r = r;
}
public void run() {
int x = 0;
while (true) {
synchronized (r) {
//判断有资源就等待
if (r.flag) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//交替存入caven和琴琴这两个人的信息
if (x == 0) {
r.name = "caven";
r.sex = "man";
} else {
r.name = "琴琴";
r.sex = "女女女女女";
}
x = (x + 1) % 2;
r.flag = true;
r.notify();//唤醒其他线程
}
}
}
}
//取出线程
class Output implements Runnable {
private Res r;
Output(Res r) {
this.r = r;
}
public void run() {
while (true) {
synchronized (r) {
//判断资源为空就等待
if (!r.flag) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(r.name + "...." + r.sex);
r.flag = false;
r.notify();
}
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Res r = new Res();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
}
}
部分结果如下所示:
五、等待唤醒机制
1、原因:因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同步才具有锁。
2、说明:Object 中的方法 notify()、notifyAll()、wait()等都在同步中使用。为什么这些操作线程的方法要定义 Object 类中呢?
答:因为这些方法在操作同步中线程时,都必须要标识它们所操作线程只有的锁,只有同一个锁上的被等待线程,可以被同一个锁上 notify 唤醒。不可以对不同锁中的线程进行唤醒。 等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法定义 Object 类中。
3、wait():在其他线程调用此对象的notify()方法或notifyAll()方法前,导致当前线程等待。换句话说,此方法的行为就好像它仅执行wait(0)调用一样。当前线程必须拥有此对象锁(监视器)。该线程发布对此监视器的所有权并等待,直到其他线程通过调用notify()方法,或notifyAll()方法通知在此对象的监视器上等待的线程醒来。然后该线程将等到重新获得对监视器的所有权后才能继续执行。
Note:
wait()方法在使用时只能 try(处理异常)不能抛异常。当出现多个线程同时操作一个对象时时,要用 while 循环和 notifyAll();比较通用的方式。定义while判断标记是为了让被唤醒的线程再一次判断标记。定义 notifyAll(), 是因为需要唤醒对方线程。只用 notify(),容易出现只唤醒本方线程的情况。导致程序中的所有线程都等待。
六、JDK5.0 升级版线程功能
1、Lock 接口:
实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构, 可以具有差别很大的属性, 可以支持多个相关的 Condition对象(可以理解为 lock 替代了 synchronized)。
2、ReentrantLock类:
Lock 的子类。一个可重入的互斥锁,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
Note:JDK1.5 中提供了多线程升级解决方案:显示的锁机制,以及显示的锁等待唤醒机制。
1)将同步 Synchronized 替换成现实 Lock 操作。
2)将 Object 中的 wait,notify notifyAll,替换了 Condition 对象。该对象可以 Lock 锁进行获取。
3)释放锁的动作一定要执行。
JDK5.0新特型改写后的生产者消费者案例:
/*
* JDK5.0 升级后的生产者消费者案例演示
*/
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//资源
class Resources {
private String name;
private int count = 1;
private boolean flag = false;
<span><span class="comment">//创建两Condition对象,分别来控制等待或唤醒本方和对方线程</span><span> </span></span>
private Lock lock = new ReentrantLock();
private Condition conSet = lock.newCondition();
private Condition conOut = lock.newCondition();
public/* synchronized */void set(String name) {
lock.lock();//上锁
try {
while (flag) {
try {
/<span><span class="comment">/本方等待</span><span> </span></span>
conSet.await();
/* wait(); */
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + "---" + count++;
System.out.println(Thread.currentThread().getName() + "生产者"
+ this.name);
flag = true;
conOut.signal();// <span><span class="comment">//唤醒对方</span><span> </span></span>
/* this.notifyAll(); */
} finally {
lock.unlock();//解锁动作,一定要执行
}
}
public/* synchronized */void out() {
lock.lock();
try {
while (!flag) {
try {
conOut.await();
/* wait(); */
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--消费者--"
+ this.name);
flag = false;
conSet.signal();
/* this.notifyAll(); */
}
} finally {
lock.unlock();
}
}
}
// 生产者
class Producers implements Runnable {
private Resources res;
public Producers(Resources res) {
this.res = res;
}
public void run() {
while (true) {
res.set("商品");
}
}
}
// 消费者
class Comsumers implements Runnable {
private Resources res;
public Comsumers(Resources res) {
this.res = res;
}
public void run() {
while (true) {
res.out();
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Resources res = new Resources();
Producers pro = new Producers(res);
Comsumers con = new Comsumers(res);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
部分结果如下所示:
3、停止线程(JDK5.0之后)
原理:只有让run()方法结束。
两种方式:
1)定义循环结束标记:因为线程运行代码一般都是循环,只要控制了循环,就可以结束 run(),也就线程结束。
2)使用 interrupt(中断)方法:该方法结束线程的冻结状态,使线程回到运行状态中来。
Note:stop()方法已过时,不再使用。
示例:
class StopThread implements Runnable {
private boolean flag = true;
// 改变flag
public void changeFlag(boolean flag) {
this.flag = flag;
}
public synchronized void run() {
int i = 0;
// 利用flag来标记线程是否继续
// 特殊情况:
// 当线程处于冻结状态的时候就不会读取到标记,即线程结束不了
while (flag) {
try {
wait();
} catch (Exception e) {
System.out.println(Thread.currentThread().getName()
+ "Exception...");
changeFlag(false);
}
System.out.println(Thread.currentThread().getName() + "...." + i++);
}
}
}
public class ThreadDemo4 {
public static void main(String[] args) {
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = 0;
while (true) {
if (num++ == 60) {
st.changeFlag(false);
// 利用interrupt来结束线程
t1.interrupt();
t2.interrupt();
break;
}
System.out.println(Thread.currentThread().getName() + "...." + num);
}
}
}
结果如下所示:
七、其他方法
1、守护线程
setDacmon(Boolean on) //将该线程标记为守护线程或用户线程。
Note:Thread 类该方法必须在启动线程前调用,当正在运行的线程都是守护线程时,java 虚拟机退出。该方法首先调用 checkAccess()方法,且不带任何参数,可能抛出SecurityException(在当前线程中)。主线程为前台线程,守护线程可以理解为后台线程。后台线程特点:开启后,与前台线程共同抢劫 CPU 的执行权运行。当所有的前台线程结束后,后台线程会自动结束。
2、join()方法
1)概述:Thread 类的final修饰的方法,等待线程终止。该方法抛异常 InterruptedException抢夺 CPU 执行权(主线程要等待 join ()执行的线程执行结束,再恢复继续运行)。一般临时加入线程时,使用 join()。
2)特点:当 A 线程执行到了 B 线程的.join()方法时,A 就会等待。等 B 线程都执行完,A才会执行。join 可以用来临时加入线程执行。
3、优先级和 yield()方法
在Thread类覆盖了toString()方法。
String toString() //返回该线程的字符串表现形式。 包括线程名称、优先级和线程组。
static void yield() //暂停当前正在执行的线程对象,并执行其他线程。减少线程的执行频率。
ThreadGroup 类:线程组。表示一个线程的集合。
优先级:代表抢资源的频率。范围:1——10。只有 1(MIN_PRIORITY)、5(NORM_PRIORITY)、10(MAX_PRIORITY)最明显。所有线程(包括主线程)默认优先级是 5。
setPriorty(int newPriority) //更改线程的优先级。
eg: t1.setPriority(Thread.MAX_PRIORITY); //设置 t1 的线程优先级为 10
本篇幅所描述的仅代表个人看法,如有出入请谅解。