进程与线程
进程
目前的操作系统是一种能够支持多任务的系统,比如在一台电脑可以同时运行很多个应用程序(比如:qq,微信,ppt,eclipse,typora),这每一个应用程序可以认为是一个正在执行的任务,比如在使用eclipse编写代码时还可以同时打开播放器听音乐,以及开启笔记工具记笔记,就目前的多核操作系统来说可以支持这些任务的同步运行;当然在单核时代,操作系统也是可以支持多个任务“同时”运行,实际运行方式是,在同一个时间片应用程序只得到了为数不多了执行时间,CUP在多个任务之间快速的切换,以至于用户跟本看不出是系统是单任务的。进程是计算机中应用程序的基本单元。
线程
一个进程中可以包含多个子任务同时运行,比如:在使用360安全卫士进行全盘扫描时,后台可以同步更新病毒库,同时检测正在运行的其他程序;其中的每一个子任务都称之为一条线程,因此,进程中至少包含一个线程,即线程是进程中的一条执行路径。举个通俗栗子:比如去食堂排队吃饭是一个进程,但是食堂的窗口有很多,每个窗口售卖不同的餐品,有卖包子的,有卖面条,有卖麻辣香锅等,每一个窗口我们可以认为是这个进程中的一条线程。
线程状态
每一条线程都有各自的生命周期,并且线程对象都存在以下几种状态:
- 初始:线程刚创建时候的状态
- 就绪:当线程准备运行时候的状态
- 运行:当CPU分配时间片给当前线程,线程进入运行态
- 阻塞:当CPU将时间片从当前线程分配到其他线程时,当前线程进入阻塞态
- 销毁:当线程执行完毕后销毁
线程的创建与启动
java中提供对线程的支持,java中创建线程包含四种方式,其中有两种是基本的创建方式,另外两种是在JDK1.5之后新增的并发编程包中,如下:
- 实现Runnable接口
- 继承Thread类
- 实现Callable接口,创建FutureTask
- 使用ExecutorService,Callable等相关接口
1.实现Runnable接口(实现run方法)
public class MyThread implements Runnable{
private String name;
public MyThread(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(name+":--->"+i);
}
}
public static void main(String[] args) {
MyThread mt1 = new MyThread("t1");
MyThread mt2 = new MyThread("t2");
// 方法调用
// mt.run();
//创建线程对象
Thread t1 = new Thread(mt1);
Thread t2 = new Thread(mt2);
//线程启动
t1.start();
t2.start();
}
}
- 继承Thread类(重写run方法)
`
public class MyThread2 extends Thread{
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(this.getName()+"---->"+i);
}
}
public static void main(String[] args) {
MyThread2 mt1 = new MyThread2();
MyThread2 mt2 = new MyThread2();
mt1.start();
mt2.start();
}
}
综合以上两种线程创建的方式,区别:
-
实现Runnable接口更灵活,类还能在实现其他接口或继承其他类,但是线程的创建和启动依然需要Thread类完成。
-
继承Thread类,相对第一种更简单,可以直接创建子类对象并启动线程,但是耦合度较高,子类不能再对其他类继承。
关于线程的启动:
必须通过调用Thread类的start方法完成,不能直接通过线程对象调用run方法(实际还是单线程的执行方法:普通方法调用)
Thread类详解
Thread类是java.lang包中用于创建线程对象的类,JVM允许同时运行多个Thread对象,Thread类从Runnable实现,因此Thread类中也对run方法做了实现,通常需要创建自定义线程时,一般需要重写run方法;Thread类中提供了一个常用构造器和方法用于操作线程对象:
- 常见构造器:
- Thread()
- Thread(String name)
- Thread(Runnable r)
- Thread(Runnable r,String name)
- 常见方法
- static int activeCount() 获取活动线程数(包含主线程)
- static Thread currentThread() 获取当前线程对象
- long getId() 获取线程的标识符
- String getName() 获取线程名称
- getPriority() 获取线程优先级(1-10)
- setPriority(int p)设置线程优先级(MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY)
- interrupt() 中断线程对象(并非真正中断,实际是添加中断标记)
- isInterrupted() 判断线程是否中断
- sleep(long time) 让休眠指定毫秒,时间到达会自动唤醒
public class MyThread4 extends Thread{
public void run() {
try {
for (int i = 0; i < 100; i++) {
// sleep((int)(Math.random()*100));
System.out.println(this.getId()+"--"+this.getName()+":"+i+"---优先级:"+this.getPriority());
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyThread4 mt1 = new MyThread4();
MyThread4 mt2 = new MyThread4();
// MyThread4 mt3 = new MyThread4();
// MyThread4 mt4 = new MyThread4();
//设置线程优先级
mt1.setPriority(Thread.MAX_PRIORITY);
mt2.setPriority(Thread.MIN_PRIORITY);
mt1.start();
mt2.start();
// mt3.start();
// mt4.start();
//统计激活线程总数
System.out.println("线程总数:"+Thread.activeCount());
//获取当前线程对象的名称
System.out.println(Thread.currentThread().getName());
}
}
线程终止
线程在运行过程中可能由于满足到一些特定条件后需要中断当前线程,因此,如何安全有效的终止线程的运行就成为一个比较重要的问题了,java多线程编程中,线程的中断包含以下几种方式:
- 标记中断法
- 异常中断法
标记中断法
这是一个较为推荐的中断方式,原理为:在线程执行前声明一个结束标记,当执行过程中满足了结束标记时,通过相关的逻辑执行结束:
public class ThreadEnd extends Thread{
@Override
public void run() {
boolean isOver = false;
int i = 0;
while(!isOver){
System.out.println(this.getName()+"-->"+i);
i++;
if(i >= 50000){
isOver = true;
return;
}
}
System.out.println("确定结束了么?这是线程内部的输出!");
}
public static void main(String[] args) {
ThreadEnd te = new ThreadEnd();
te.start();
}
}
异常中断法
异常中断法即通过异常的抛出将正在执行的线程中断:
public class ThreadEnd2 extends Thread{
@Override
public void run() {
try {
for (int i = 0; i < 1000000; i++) {
System.out.println(this.getName()+"-->"+i);
if(this.isInterrupted()){
throw new InterruptedException("当前线程被中断!!!");
}
}
} catch (InterruptedException e) {
System.out.println("线程终止");
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
ThreadEnd2 te2 = new ThreadEnd2();
te2.start();
Thread.sleep(2000);
//中断线程
te2.interrupt();
}
}
另外还有一种可以直接使用Thread类中的stop(过时的)方法,强行终止线程,但是stop极为不安全,当使用stop方法终止线程时,将会导致对象锁被释放,从而产生一些不可预知后果。
守护线程
守护线程也称之为后台线程,守护线程的主要作用于为其他线程提供服务,当守护线程守护的主线程结束后,守护线程也将结束(皮之不存毛将焉附),比如,文件下载时,一条主线程实现文件拷贝,另一条子线程用于计算下载进度,该条子线程主要为下载线程提供服务,因此计算下载进度的线程可以定义为守护线程;只需要调用Thread类提供的setDaemon()方法即可实现,java中要将其他线程设置为守护线程,例如:
后台线程类:
public class BackService extends Thread{
@Override
public void run() {
int i = 1;
while(true){
try {
System.out.println("后台线程(守护线程)正在执行:"+i);
i++;
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
主任务线程:
public class ThreadDeamon extends Thread{
@Override
public void run() {
//创建线程对象
BackService bs = new BackService();
//设置当前线程为守护线程
bs.setDaemon(true);
bs.start();
try {
for (int i = 0; i < 1000; i++) {
sleep(2);
System.out.println("执行线程---->"+i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new ThreadDeamon().start();
}
}
以上程序中如果主任务线程执行结束了,则不论守护线程是否达到结束条件,都会随之终止。
通过实例得出,所谓守护线程即将普通线程设置为后台运行即可(调用setDaemon方法)
多线程编程
多线程实现文件修改监听
public class FileLinstener2 implements Runnable{
private File file;
public FileLinstener2(File file) {
this.file = file;
}
@Override
public void run() {
//获取当前文件最后修改时间
long time = file.lastModified();
while(true){
try {
long now = file.lastModified();
if(now != time){
//文件被修改
System.out.println(file.getName()+" file changed,"+getTime(now));
//将原来的时间设置为最新时间
time = now;
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**格式化日期*/
public String getTime(long time){
DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d = new Date(time);
return fmt.format(d);
}
public static void main(String[] args) {
//目标目录
File file = new File("C:\\Users\\mrchai\\Desktop\\tempfile");
//获取目录中所有子文件
File[] files = file.listFiles();
for (File f : files) {
//为每一个File对象启动一条监听线程
FileLinstener2 lis = new FileLinstener2(f);
Thread t = new Thread(lis);
t.start();
}
}
}
线程并发
线程并发
实际开发中,有很多场景都会出现多个线程共享相同内存空间(变量)的情况,由于多个线程并发操作相同的内存空间,此时就极有可能出现数据不安全的问题,比如经典的银行取钱问题:
有一个银行账户,还有余额1100元,现在A通过银行卡从中取1000元,而同时另外一个人B通过存折也从这个账户中取1000元。取钱之前,要首先进行判断:如果账户中的余额大于要取的金额,则可以执行取款操作,否则,将拒绝取款。
我们假定有两个线程来分别从银行卡和存折进行取款操作,当A线程执行完判断语句后,获得了当前账户中的余额数(1000元),因为余额大于取款金额,所以准备执行取钱操作(从账户中减去1000元),但此时它被线程B打断,然后,线程B根据余额,从中取出1000元,然后,将账户里面的余额减去1000元,然后,返回执行线程A的动作,这个线程将从上次中断的地方开始执行:也就是说,它将不再判断账户中的余额,而是直接将上次中断之前获得的余额减去1000。此时,经过两次的取款操作,账户中的余额为100元,从账面上来看,银行支出了1000元,但实际上,银行支出了2000元。
编码重现问题:
Account.java
/**
* 账号类
* @author mrchai
*
*/
public class Account {
private int id;
private double money;
public Account() {
// TODO Auto-generated constructor stub
}
public Account(int id, double money) {
super();
this.id = id;
this.money = money;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
AccountManager类
public class AccountManager {
//需要被操作的账户对象
private Account account;
public AccountManager(Account account) {
super();
this.account = account;
}
public void getMoney(double cash){
System.out.println("线程进入:"+Thread.currentThread().getName());
//判断余额是否足够
if(account.getMoney() >= cash){
double d = account.getMoney() - cash;
System.out.println("取款成功,取款:"+cash+",余额"+d);
account.setMoney(d);
}else{
System.out.println("取款失败,余额不足");
}
}
}
PeopleThread类
public class PeopleThread extends Thread{
private AccountManager manager;
private double cash;
public PeopleThread(AccountManager manager,double cash) {
super();
this.manager = manager;
this.cash = cash;
}
@Override
public void run() {
manager.getMoney(cash);
}
public static void main(String[] args) {
//初始化一个账户(确保只有一个账户)
Account a = new Account(1,10000);
AccountManager manager = new AccountManager(a);
//创建两个线程对象,传入操作账户的管理对象(唯一)和取款金额
PeopleThread t1 = new PeopleThread(manager,10000);
PeopleThread t2 = new PeopleThread(manager,10000);
t1.start();
t2.start();
}
}
结果
`
线程进入:Thread-1
线程进入:Thread-0
取款成功,取款:10000.0,余额0.0
取款成功,取款:10000.0,余额0.0
通过观察结果,得知,账户实际只有10000元,但是两个线程都取款成功,这就出现了线程并发数据不一致的严重问题,如何解决问题?
Synchronized
以上银行取款问题只是众多线程并发产生的安全问题中的一种,还有很多情况,比如:秒杀,抢券,抢票;因此在这问题产生后,就需要一种排队机制的引入;在Java中每一个对象都存在一个互斥锁(排他锁,对象锁),具体的使用是通过synchronizad关键字将对象锁定,此时,如果对象被一个线程锁定,则其他线程无法在操作当前对象,只有等待拥有该对象锁的线程释放锁之后,其他线程才能使用该对象。
以上程序可以如下处理:
public void getMoney(double cash){
//将对象锁定,当前线程释放对象锁之前,其他线程无法访问
synchronized (account) {
System.out.println("线程进入:"+Thread.currentThread().getName());
//判断余额是否足够
if(account.getMoney() >= cash){
double d = account.getMoney() - cash;
System.out.println("取款成功,取款:"+cash+",余额"+d);
account.setMoney(d);
}else{
System.out.println("取款失败,余额不足");
}
}
}
以上操作即称之为线程同步。因此,synchronized语句块也称之同步块,synchronized不仅可以用于同步块,还能适用于同步方法,即在方法的声明上使用该关键字,如下
public synchronized void getMoney(double cash){
System.out.println("线程进入:"+Thread.currentThread().getName());
//判断余额是否足够
if(account.getMoney() >= cash){
double d = account.getMoney() - cash;
System.out.println("取款成功,取款:"+cash+",余额"+d);
account.setMoney(d);
}else{
System.out.println("取款失败,余额不足");
}
}
死锁
多线程并发时,除了容易出现线程安全和数据不同问题外,还容易出现另一种情况:多个线程都在等待被对方占有的资源释放,此时程序会陷入一个僵局,这种状态就称之为死锁,死锁经典问题:哲学家吃饭问题
package com.softeem.lesson22.sync;
public class DeadLock implements Runnable{
private int id;
//声明两个静态的引用类型对象
private static Object obj1 = new Object();
private static Object obj2 = new Object();
public DeadLock(int id) {
super();
this.id = id;
}
@Override
public void run() {
if(id == 1){
synchronized (obj1) {
try {
System.out.println("线程"+id+"已锁定obj1,等待锁定obj2");
Thread.sleep(1000);
synchronized (obj2) {
System.out.println("线程"+id+"执行结束");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}else{
synchronized (obj2) {
try {
System.out.println("线程"+id+"已锁定obj2,等待锁obj1");
Thread.sleep(1000);
synchronized (obj1) {
System.out.println("线程"+id+"执行结束");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
DeadLock d1 = new DeadLock(1);
DeadLock d2 = new DeadLock(2);
new Thread(d1).start();
new Thread(d2).start();
}
}
wait & notify
wait和notify(notifyAll)是Object类中的方法,wait和notify在调用时,当前线程对象必须拥有该对象的对象锁,否则会出现IllegalMonitorStateException异常,具体案例:
有三个等待线程处于等待状态,有一个唤醒线程等待指定秒之后唤醒处于等待状态的线程:
WaitThread.java
public class WaitThread extends Thread{
private Object obj;
private String name;
public WaitThread(String name,Object obj) {
super();
this.obj = obj;
this.name = name;
}
@Override
public void run() {
System.out.println(name+"进入run...");
//CAS锁(乐观锁)
//悲观锁
synchronized (obj) {
try {
System.out.println(name+"等待。。。");
//只有当前线程拥有指定对象的锁(对象监视器)才能调用wait方法,并且wait一旦执行
//该对象的锁就会被释放
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name+"执行结束。。");
}
}
NotifyThread
public class NotifyThread extends Thread{
private Object obj;
public NotifyThread(Object obj) {
super();
this.obj = obj;
}
@Override
public void run() {
System.out.println("唤醒线程启动,准备唤醒其他在该对象上的线程...");
synchronized (obj) {
try {
sleep(5000);
System.out.println("唤醒");
//唤醒在当前对象上的所有线程,同时释放对象锁
obj.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("唤醒线程执行完成!");
}
}
Test.java
public class Test {
public static void main(String[] args) {
//被监视对象是唯一的
Object obj = new Object();
//等待线程
WaitThread wt = new WaitThread("线程A", obj);
WaitThread wt2 = new WaitThread("线程B", obj);
WaitThread wt3 = new WaitThread("线程C", obj);
wt.start();
wt2.start();
wt3.start();
//唤醒线程
NotifyThread nt = new NotifyThread(obj);
nt.start();
}
}