- 一、线程概述
多进程:在操作系统中能(同时)运行多个任务(程序)
多线程:在同一应用程序中有多个顺序流(同时)执行,即:一段代码在不同的顺序流上同时运行或者是不同的代码在不同的顺序流上同时在运行,其中顺序流的学名称为线程。
例如:QQ聊天软件,聊天与接收信息是同时进行的。
线程:同一个程序内部的多个代码段同时运行
程序对应的是进程,程序内部的才叫线程,线程的粒度比进程的粒度要小
不同程序之间的线程是不能进行通信的,线程的通信指的是同一个程序内部的
- 二、Java线程模型
虚拟的CPU,由java.lang.Thread类封装和虚拟CPU(JVM)来实现。
CPU所执行的代码,传递给Thread类对象。
CPU所处理的数据,传递给Thread类对象。
- 三、创建线程
通过java.lang.Thread来创建一个线程
Thread的构造器:
Thread()
Thread(Runnable target)
Thread(Runnable target, String name)
Thread(String name)
Thread(ThreadGroup group, Runnable target) //ThreadGroup线程组
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, String name)
一般不建议直接使用thread构造方法来创建线程
实现线程的方法:
- 继承Thread类,实现run()方法
- 实现Runnable接口
run()方法不是由你调用的,而是由线程模型中虚拟的CPU(JVM)自动调用的
//currentThread()方法用于返回当前的线程对象
System.out.println(Thread.currentThread().getName());
//getName()方法是Thread类的一个方法,该方法返回当前线程的名称
System.out.println(this.getName());
多线程的特点:多段代码交替运行
创建一个线程:
- 第一步,创建线程对象;
- 第二步,通过线程对象的start()方法启动线程(这个启动不是说立即就运行了,只是注册在线程队列中,由JVM自动调度)
每个线程都是通过某个特定Thread对象所对应的run( )方法来完成其操作的,run( )方法称为线程体。所以需要把在新线程上运行的程序代码都放在run()方法中,这样才能保证这些代码在一个新的线程上运行。
使用start()方法,线程进入Runnable(可运行)状态,它将向线程调度器注册这个线程。
调用start()方法并不一定马上会执行这个线程,正如上面所说,它只是进入Runnable 而不是Running。
注意:不要直接在程序中调用线程的run()方法。
【继承Thread类】
创建并启动线程示例
public class TestThread extends Thread{
public void run(){
for(int i = 0;i<100;i++)
System.out.println("Count:"+i);
}
public static void main(String[] args){
TestThread tt = new TestThread();
//注意,不要直接调用run方法
tt.start();
}
}
【实现Runnable接口】
通过实现Runnable接口并实现接口中定义的唯一run()方法,从而可以创建一个线程。
public class RunnableThread implements Runnable{
//实现接口Runnable中的run方法
public void run(){
for (int k = 0;k<10;k++){
System.out.println("Count:"+k);
}
}
//可以根据需要添加其它方法
}
当一个类实现了Runnable接口,它可以被放到线程上运行,但它本身是不能够启动一个线程的,需要通过Thread类来启动
RunnableDemo rd = new RunnableDemo();
Thread t = new Thread(rd);
t.start();
【两种创建线程方式的比较】
使用Runnable接口
- 可以将代码和数据分开(即:线程对象和实现了Runnable接口的类的对象),形成清晰的模型;
- 还可以从其他类继承;
- 保持程序风格的一致性。
- 如果要返回当前线程对象,需要使用Thread.currentThread()方法
直接继承Thread类
- 不能再从其他类继承;
- 编写简单,可以直接操纵线程,无需使用Thread.currentThread()方法返回当前线程对象。
注:推荐使用实现Runnable接口的方式来实现多线程
【结束线程】
线程会以以下三种方式之一结束:
- 线程到达其 run() 方法的末尾,即run()方法的最后一条代码执行完毕;
- 线程抛出一个未捕获到的Exception及其子类的异常对象 或 Error及其子类的异常对象;
- 另一个线程调用一个Deprecated(过时)的stop() 方法。(某些情况下容易产生死锁)
【线程的五种状态】
Linux中存在九种状态,它比Window系统(五种)的效率更高,更稳定
- 四、后台线程(守护线程)
有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又称为“守护线程”。
主要是为主线程提供数据服务的
DaemonThread dt = new DaemonThread();
//将此线程设置为后台线程
dt.setDaemon(true); //1
dt.start();
注意:先设置成后台线程,后调用start()方法启动线程。需要说明的是,如果只有后台线程,而没有主线程(前台线程)时,则程序将会结束。
当主线程消亡后,后台线程不会立即结束,会运行一会儿后侦测到主线程不存在再结束
public static void main(String[] args) {
Runnable_Demo rd = new Runnable_Demo();
Thread t1 = new Thread(rd,"后台线程1");
t1.setDaemon(true);
t1.start();
for(int i = 0; i < 10; i++){
System.out.println("main is running...");
}
}
【线程的简单控制】
测试线程是否正处于Runnable状态
- isAlive()
中断线程
- Thread.sleep(毫秒):使当前线程休眠指定的毫秒数
- Thread.yield():当线程进入阻塞队列需要人为唤醒
设置线程的优先级
- getPriority()
- setPriority()
- 五、线程的join()方法
- Thread API 包含了等待另一个线程完成的方法:join() 方法。当调用 join() 方法时,调用线程(即调用join()方法这行代码所在的线程)将阻塞,直到被join方法加入的目标线程(调用join()方法的线程对象)执行完成为止。 (新线程.join(),主线程等待,先调用start()方法,后join()方法)
- Thread.join()方法: 通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。
JoinThread r = new JoinThread();
Thread t = new Thread(r);
t.start();
try{
t.join();//合并,当前线程阻塞,加入的线程不执行完毕当前线程永远不会执行
}catch(InterruptedException e){
……
}
示例
public class Test_Join_Demo {
public static void main(String[] args) {
int i = 0;
JoinTest jt = new JoinTest();
Thread t1 = new Thread(jt);
t1.start();
while(true){
try {
if(i == 100){
//将t1线程合并到main方法所在的线程(即主线程)中,t1线程不运行完,main方法所在的线程(即主线程)不会运行。
t1.join();
//t1线程与main方法所在的线程(即主线程)合并1秒后,再度分离成两个线程,即过1秒钟后,t1线程与main方法所在的线程(即主线程)又开始交替运行
//单位为:毫秒
//t1.join(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main is running...->i=" + i++);
}
}
}
class JoinTest implements Runnable{
@Override
public void run() {
while(true){
System.out.println(Thread.currentThread().getName());
}
}
}
- 六、多线程编程
多线程编程通常指:
- 多个线程来自同一个Runnable实例
- 多个线程使用同样的数据和代码
注:多线程编程,使用的实现方式一般都使用实现Runnable接口的方式,这样才能保证多个线程共享同一个实例对象。
例子:
Thread t1 = new Thread(object1);
Thread t2 = new Thread(object1);
【多线程共享数据】
继承Thread类实现线程方式达不到多线程共享数据,是因为不同线程上放的是不同的对象;而实现Runnable接口方式可以实现多线程共享数据则是因不同线程上放的是同一个对象
//无法实现多线程共享
TestThread t1 = new TestThread();
TestThread t2 = new TestThread();
t1.start();
t2.start();
//可以实现多线程共享
… …
RunningObject ro = new RunningObject();
Thread t1 = new Thread(ro,"1st");
Thread t2 = new Thread(ro,"2nd");
t1.start();
t2.start();
… …
说明:本例是把ro对象分别放到2个线程上去运行,从而这2个线程共享同一个ro对象。
- 七、线程间同步
同步控制主要控制的是成员变量,因成员变量存储的是数据,当多线程共享时数据存在危险
夫妻存款示例
package com.qian;
class Mycount implements Runnable {
int mymoney = 1000;
public void run() {
for (int i = 1; i <= 10; i++) {
try {
synchronized (this) {
int hold;
hold = mymoney + 100;
Thread.sleep(2000);
mymoney = hold;
System.out.println(Thread.currentThread().getName() + "存了"
+ "100元后帐户有:" + +mymoney + "元");
System.out.println("----------------------------");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
package com.qian;
public class HusbandandWife {
public static void main(String[] args){
Mycount myc = new Mycount();
Thread husband = new Thread(myc,"丈夫");
Thread wife = new Thread(myc,"妻子");
husband.start();
wife.start();
}
}
【互斥锁】
在Java语言中,引入了对象互斥锁(mutual exclusive lock,也简称为对象锁)的概念,来保证共享数据操作的完整性:
- 每个对象都有一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
- 关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问。
【关键字synchronized】
在Java中的两种使用synchronized的方式:
- 放在方法前面,这样,调用该方法的线程均将获得对象的锁。(此时这个对象锁就是当前对象自己,即this)
- 放在代码块前面(此时代码块称为:同步代码块),它也有两种形式:
- synchronized (this){… …} 代码块中的代码将获得当前对象引用的锁
- synchronized(otherObj){… …}:代码块中的代码将获得指定对象引用的锁
同步方法不能人为指定锁,同步方法使用的锁就是当前对象自己
当同步代码块的锁设置为this时,则一个类中的同步代码块与同步方法用的同一把锁(进行统一控制)
当同步代码块与同步方法操作了同一成员变量则需要使用同一把锁进行控制,即上一句的情况
同步代码块与同步方法的两个区别:
- 同步代码块非常灵活,控制的锁可以随便指定,而同步方法的锁不能指定,就是当前对象自己
- 同步代码块(限制范围小)与同步方法(限制范围大)谁的效率高?同步代码块效率更高
因此要尽量使用同步代码块
任何临界区都是有锁的
【释放锁】
- 如果一个线程一直占用一个对象的锁,则其他的线程将永远无法访问该对象,因此,需要在适当的时候,将对象锁归还。
- 当线程执行到synchronized()块结束时,释放对象锁。
- 当在synchronized()块中遇到break, return或抛出异常,则自动释放对象锁。
- 当一个线程调用wait()方法时,它放弃拥有的对象锁并进入等待队列(等待该对象锁的等待队列中)。
一wait()必须notify()(以组出现的)
notify():激活等待队列中的第一个线程
notifyAll():激活等待队列中的所有线程
【线程死锁】
- 是指两个或多个线程,都相互等待对方释放lock
- 是不可测知或避开的
- 应采取措施避免死锁的出现
任何对象都能当锁
出现死锁情况示例
public class DeadLock_Demo implements Runnable{
private A a = new A();//A锁
private B b = new B();//B锁
int i = 1;
public void run() {
if(i == 1){
synchronized(a){
i = 2;
try {
Thread.sleep(1000);
synchronized(b){
for(int i = 0; i< 10; i++){
if(i ==0){
System.out.println(Thread.currentThread().getName() + "---" + i);
}
System.out.println(i);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}else{
synchronized(b){
try {
Thread.sleep(1000);
synchronized(a){
for(int i = 0; i< 10; i++){
if(i ==0){
System.out.println(Thread.currentThread().getName() + "---" + i);
}
System.out.println(i);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Test_DeadLock {
public static void main(String[] args) {
DeadLock_Demo dl = new DeadLock_Demo();
Thread t1 = new Thread(dl);
Thread t2 = new Thread(dl);
t1.start();
t2.start();
}
}
【线程控制的有关方法】
方法 | 说明 |
start() | 新建的线程进入Runnable状态 |
run() | 线程进入Running 状态 |
wait() | 线程进入等待状态,等待被notify,这是一个对象方法,而不是线程方法,当前线程会放弃所持有的对象锁 |
notify()/notifyAll() | 唤醒其他的线程,这是一个对象方法,而不是线程方法 |
yield() | 线程放弃执行,使其他优先级不低于此线程的线程有机会运行,它是一个静态方法 |
getPriority()/setPriority() | 获得/设置线程优先级 |
suspend() | 挂起该线程,Deprecated,不推荐使用,不会释放所持有对象锁 |
resume() | 唤醒该线程,与suspend相对,Deprecated,不推荐使用 |
sleep() | 线程睡眠指定的一段时间,但当前线程不会放弃所持有的对象锁 |
join() | 调用这个方法的主线程,会等待加入的子线程完成 |
- 八、线程间通信
【线程间通讯】
- 什么是线程间通讯:几个或多个线程之间相互通知由谁来干活谁休息
- Object 类定义了 wait()、notify() 和 notifyAll() 方法。可以让线程相互通知事件的发生。要执行这些方法,必须拥有相关对象的锁。
- wait() 会让调用线程休眠,直到用 Thread.interrupt() 中断它、过了指定的时间、或者另一个线程用 notify() 或 notifyAll() 唤醒它,锁被释放。
- 当对某个对象调用 notify() 时,如果有任何线程正在通过 wait() 等待该对象,那么就会唤醒其中一个线程。当对某个对象调用 notifyAll() 时,会唤醒所有正在等待该对象锁的所有线程。
【wait、notify、notifyAll说明】
wait、notify、notifyAll这三个方法只能在synchronized方法中调用,即无论线程调用一个对象的wait还是notify方法,该线程必须先得到该对象的监视器(锁旗标),这样notify只能唤醒同一对象监视器中调用wait的线程,使用多个对象监视器,就可以分组有多个wait、notify的情况,同组里的wait只能被同组的nofity唤醒。
【避免无谓的同步方法】
- 因为同步会降低程序的执行效率,所以应该避免无谓的同步
- 通过所谓的Fine-Grained锁的机制,可以避免这种情况
- Fine-Grained(细粒度)锁机制:Synchronized方法->synchronized代码块
【多线程编程一般规则】
- 如果两个或两个以上的线程都修改一个对象,那么把执行修改的方法定义为被同步的,如果对象更新影响到只读方法,那么只读方法也要定义成同步的。
- 不要滥用同步。如果在一个对象内的不同的方法访问的不是同一个数据,就不要将方法设置为synchronized的。
- 如果一个线程必须等待一个对象状态发生变化,那么他应该在对象内部等待,而不是在外部。他可以通过调用一个被同步的方法,并让这个方法调用wait()。
- 每当一个方法返回某个对象的锁时,它应当调用notifyAll()来让等待队列中的其他线程有机会执行。
- 记住wait()和notify()/notifyAll()是Object类方法,而不是Thread类的方法。仔细查看每次调用wait()方法,都有相应的notify()/notifyAll()方法,且它们均作用于同一个对象。
- 针对wait()、notify()/notifyAll()使用旋锁(spin lock),所谓旋锁就是指使用对象的对象锁;
- 优先使用notifyAll()而不是notify();
- 按照固定的顺序获得多个对象锁,以避免死锁;
- 不要对上锁的对象改变它的引用;
- 不要滥用同步机制,避免无谓的同步控制。