1. 线程基础
1.1 进程
进程就是应用程序的执行实例,它的特点是:动态性,并发性和独立性
1.2 线程
线程是进程内部的一个执行单位,它是程序中一个单一的顺序控制流程。
它的特点:
*一个进程可以包含多个线程,而一个线程至少要一个父进程
*线程可以有自己的堆栈,程序计数器和局部变量
*线程与父进程的其他线程共享进程所有的全部资源,包括内存
*独立运行,采用抢占方式
*一个线程可以创建和删除另外一个线程
*同一个进程中的多个线程之间可以并发执行
*线程的调度管理是由进程来完成的
注意:编程时必须确保线程不会妨碍同一进程的其他线程
小结:
线程是比进程小一级的单位,它不可以独立存在,一个进程可以包含多个线程,这些线程和它对应的进程共享一个存储空间,因此在数据交换、通信方面,线程比进程更具优势。线程是并发执行的,当然在一个cpu的情况下,它只是在逻辑上“同时”运行,在物理上任然是串行的,即各线程在不同时间片段轮流占有cpu资源。在多个cpu的情况下,各个线程在同一时间段内运行在不同cpu上,这才真正做到并发执行。多线程的优点就是提高了cpu的执行效率,提升了应用系统的运行效率。那么在什么情况下应用Java多线程编程呢?比如说要循环播放一段背景音乐,服务器向客户端推送消息广告,在以前的项目中,还用到过多线程分发邮件,后台管理员向会员分发系统邮件,像这些需要重复完成的动作就可以使用多线程来提高系统的性能。当然还有一些复杂,费时的操作也都可以使用多线程来完成。
2. 线程的创建
2.1 创建线程的第一种方法(继承Thread类)
1、定义Thread的子类,重写父类的run()方法
2、创建Thread的子类的对象
3、调用start()方法启动该线程
class Thread1 extends Thread {
private String accoutId;
private String accoutName;
@Override
public void run() {
super.run();
for (int i = 0; i < 5; i++) {
System.out.println("线程"+this.getName()+"第"+i+"次出现。");
}
}
public String getAccoutId() {
return accoutId;
}
public void setAccoutId(String accoutId) {
this.accoutId = accoutId;
}
public String getAccoutName() {
return accoutName;
}
public void setAccoutName(String accoutName) {
this.accoutName = accoutName;
}
}
public class MyThread1{
public static void main(String[] args) {
Thread1 thread1 = new Thread1();
thread1.start();
}
}
2.2 创建线程的第二种方法(实现Runnable接口)
1、定义Runnable接口的实现类
2、重写run()方法
3、用 new Thread(Runnable的实现类)的方式实例化一个Thread类的线程对象
4、调用start()方法启动该线程
class Thread2 implements Runnable {
private String accoutId;
private String accoutName;
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程"+Thread.currentThread().getName()+"第"+i+"次出现。");
}
}
public String getAccoutId() {
return accoutId;
}
public void setAccoutId(String accoutId) {
this.accoutId = accoutId;
}
public String getAccoutName() {
return accoutName;
}
public void setAccoutName(String accoutName) {
this.accoutName = accoutName;
}
}
<pre name="code" class="java">public class MyThread2{
public static void main(String[] args) {
Thread thread2 = new Thread(new Thread2());
thread2.start();
}
}
2.3 两种创建线程的方法的比较
优点 | 缺点 | |
第一种方法(继承Thread类) | 编写简单,可以使用this直接访问当前线程 | 无法再继承其他类 |
第二种方法(实现Runnable接口) | 可以继承其他类,多个线程之间可以使用同一个Runnable对象 | 编程方式复杂,不可以直接访问当前对象,要通过currentTread()访问 |
3. 线程的状态和生命周期
3.1 线程的状态
1、新生态
创建一个线程类的的时候,它的状态就是新生态。它可以通过start( )方法进入就绪状态,上图中应该在新生态和可运行状态之间加上一个就绪状态。
2、就绪状态
处于就绪状态的线程就已经具备的运行的条件,它有机会被分配到cpu资源变成运行状态。
3、运行状态
运行状态的线程会占用cpu,执行run( )方法中的内容。处于运行状态的线程又可以转变成就绪状态、阻塞状态和死亡状态。
运行状态的线程通过yield( )方法,主动让出cpu资源,重新回到就绪状态,并和其他线程重新争夺cpu资源。
运行状态的线程可以通过sleep( )方法让线程睡眠,并放弃它所占的系统资源,此时,该线程的状态为阻塞状态。当然还有其他情况可使运行状态的线程变成阻塞状态,比如,在I/O上阻塞,等待I/O操作完成,或者该线程正在等待通知notify( )唤醒,还有该线程调用suspend( )方法。
4、阻塞状态
处于运行状态的线程如果执行了sleep()方法、suspend( )方法,或者等待I/O操作完成,该线程将让出CPU资源并暂时停止自己的运行,进入阻塞状态。阻塞状态不会直接变成就绪状态与其他线程争夺cpu资源,它只会等到睡眠时间到了,或者I/O操作完成后,在转为就绪态与其他线程竞争。
5、死亡状态
当线程的run( )方法执行完以后,该线程就进入死亡状态。
3.2 线程的优先级
* 默认情况下,一个线程继承其父类的优先级
* 优先级表示为一个整数值
* 优先级越高,执行的机会越大,反之,执行机会越小
* 高优先级的线程可以抢占低优先级线程的cpu资源
* Java提供10个优先等级,最大等级10,最小等级1,一般为5
线程的优先级与线程执行的效率没有必然的联系
4. 线程调度的常用方法
1、Join() 非静态方法,使用具体的进程对象调用
join()方法的特点是:
* 当join()执行时,当前正在进行的进程会被挂起,让调用join方法的进程执行
* join进来的进程没有执行完,会一直阻塞当前进程
2、Sleep() 静态方法,可以使用在run()方法中,实现当前进程的挂起,阻塞指定的时间
语法: Thread.sleep( long millis )
sleep()方法使当前进程进入睡眠状态--被阻塞状态,则此时其他等待的进程皆有机会执行。sleep()方法要进行异常处理
3、Yield() 使当前线程进入可运行状态,当前进程本身仍然有机会再次只是暂停当前线程,
语法: Thread.yield();
4、setDaemon() 设置当前线程对象为后台进程
语法:线程对象.setDaemon(boolean on);
5、Sleep()和yield()方法的比较
* sleep()方法使当前线程转入被阻塞的状态,它释放了系统资源。而yield()方法使当前线程转入可运行状态
* sleep()方法总是强制当前线程停止执行,而yield()方法不一定
* sleep()方法可以使其他等待运行的线程有同样的执行机会,而yield()方法只使相同或者更高优先级的线程获得执行机会
* 使用 sleep 方法时需要捕获异常,而 yield 方法无需捕获异常
5. 线程的同步
5.1 同步方法
语法:
访问修饰符 synchronized 返回类型 方法名()
synchronized 访问修饰符 返回类型 方法名()
5.2 同步代码块
语法:
Synchronized (指定要收益的对象){ 同步语句块 }
5.3 存钱和取钱为什么要使用进程同步呢?由于各线程和它们对应的进程是共享一个存储空间的,也就是说当各线程访问并修改同一个变量时,会出现数据不同步的问题。举个最经典的例子,存钱和取钱。有一个账户,初始有1000元钱,tom在ATM机上取了400,正在ATM吐钱的时候,jerry在另一个ATM机上也取了400,但是他发现这个账户上任然是1000元,好了,问题就来了,明明tom取了400,按理说jerry再取的时候账户总金额应该是600才对,这就是数据不同步引起的数据冲突问题。用线程操作共享存储变量时一样的道理,下面我们用存取钱的例子还原一下这个问题。
public class MyThread2{
public static void main(String[] args) {
Account account = new Account();
account.setAccoutAmount(1000.00);//初始账户1000.00元
Thread2 thread2 = new Thread2();//Runnable的实现类
thread2.setAccount(account);
Thread tom = new Thread(thread2,"tom");//tom线程
Thread jerry = new Thread(thread2, "jerry");//jerry线程
Thread jack = new Thread(thread2, "jack");//jack线程
Thread rose = new Thread(thread2, "rose");//rose线程
tom.start();
jerry.start();
jack.start();
rose.start();
}
}
class Thread2 implements Runnable {
private Account account;
public void run() {
getMoney(400.00);//取钱
}
//取钱
<span style="font-family:宋体;"> </span>public void getMoney(double money){
if ((account.getAccoutAmount() - money)<=0) {
System.out.println(Thread.currentThread().getName()+"的账户余额不足。");
return;
}
try {
System.out.println(Thread.currentThread().getName()+"的账户上有"+account.getAccoutAmount()+" 元钱。");
account.setAccoutAmount(account.getAccoutAmount() - money);
System.out.println(Thread.currentThread().getName()+"取走了"+money+"元,还剩"+account.getAccoutAmount()+" 元钱。");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//存钱
public void setMoney(double money){
account.setAccoutAmount(account.getAccoutAmount() + money);
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
}
class Account{
private String accoutId;
private String accoutName;
private double accoutAmount;
public String getAccoutId() {
return accoutId;
}
public void setAccoutId(String accoutId) {
this.accoutId = accoutId;
}
public String getAccoutName() {
return accoutName;
}
public void setAccoutName(String accoutName) {
this.accoutName = accoutName;
}
public double getAccoutAmount() {
return accoutAmount;
}
public void setAccoutAmount(double accoutAmount) {
this.accoutAmount = accoutAmount;
}
}
运行几次,出现的结果都不一样,这是因为线程的开始和结束时无法预测的。但问题基本都是一样
的,就是tom,jerry,jack,rose取钱的时候账户总额都是1000元,而此时他们都有取钱的动作,这与事实是不相符的。
jack的账户上有1000.0 元钱。
jack取走了400.0元,还剩600.0 元钱。
jerry的账户上有1000.0 元钱。
tom的账户上有1000.0 元钱。
tom取走了400.0元,还剩-200.0 元钱。
rose的账户上有1000.0 元钱。
rose取走了400.0元,还剩-600.0 元钱。
jerry取走了400.0元,还剩200.0 元钱。
为了解决这个问题,我们需要用到多线程的同步(synchronized关键字),具体操作就是在取钱和存钱的方法前加上 synchronized关键字,加上 synchronized关键字表示这个方法是同步方法,也就是 一个时候只允许一个线程使用这个方法。它的运行机制是,当一个线程执行这个同步方法时,它就获得了这个方法的同步锁,除非它把这个方法执行完,把锁释放掉,否则别的线程是得不到这个同步锁然后执行方法的。这样同步了的方法一次只允许一个线程执行它,就解决了数据不同步引起的数据冲突问题。具体实现如下:
//取钱
synchronized public void getMoney(double money){
if ((account.getAccoutAmount() - money)<=0) {
System.out.println(Thread.currentThread().getName()+"的账户余额不足。");
return;
}
try {
System.out.println(Thread.currentThread().getName()+"的账户上有"+account.getAccoutAmount()+" 元钱。");
account.setAccoutAmount(account.getAccoutAmount() - money);
System.out.println(Thread.currentThread().getName()+"取走了"+money+"元,还剩"+account.getAccoutAmount()+" 元钱。");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//存钱
synchronized public void setMoney(double money){
account.setAccoutAmount(account.getAccoutAmount() + money);
}
就是在取钱和存钱方法前加上
synchronized关键字,出现的结果是这样的。
tom的账户上有1000.0 元钱。
tom取走了400.0元,还剩600.0 元钱。
jack的账户上有600.0 元钱。
jack取走了400.0元,还剩200.0 元钱。
rose的账户余额不足。
jerry的账户余额不足。
5.4 死锁
虽然synchronized关键字能解决数据同步的问题,但是它的使用影响了线程并发执行的特性,而且,不恰当的使用synchronized会引起死锁。那么什么是死锁?死锁就是两个或多个线程之间,相互占有资源,又相互等待对方释放资源,各线程因此都处于阻塞状态,产生了死锁。举个例子来说,有A,B两个线程,A线程运行并占有a资源,睡眠1秒。然后B线程运行并占有b资源,接着它也睡眠1秒。之前A线程睡眠1秒之后需要得到b资源,但此时b资源已经被B占据,没有释放,所以A线程等待B线程释放b资源。B线程睡眠了1秒之后又需要a资源,但是a资源一直被A线程占据,没有释放,所以B线程等待A线程释放a资源。而此时A线程一直在等B线程释放b资源,A ,B线程都在等待对方释放资源,这就是死锁。
因此在多线程编程时应特别注意死锁的产生。
6. 线程之间的通信
线程间的通信首先建立在同步的范围内
6.1 线程间通信的实现
1、wait()方法 释放已有共享资源对象的锁,进入wait队列
2、notify()方法 随机唤醒
3、notifyAll()方法 唤醒全部
wait() 使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。调用 wait( ) 方法,会释放掉已有共享资源对象的锁,进入阻塞状态,它只会等待超出指定时间或者被notify()唤醒才会有机会重新获得锁进入可执行状态。调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。