Java的进程与线程

1. 概念

进程正在进行中的程序,运行一个程序就启动了一个或多个进程
线程一个程序内部的执行路径,如QQ管家的杀毒和清理垃圾功能
单线程一个进程中只有一个执行路径,如Java中的Main方法
多线程一个进程中有多个线程在极短的时间内交替进行
多进程在操作系统中能同时运行多个程序

tips:每个进程都有独立代码和数据空间(进程上下文),进程间切换开销大。
tips:同一进程内的多个线程共享相同的代码和数据空间,线程间切换开销小。

2. 线程

线程分为前台线程和后台线程。

前台线程中,先执行主线程,然后主线程随机分配时间片,最后子线程交替运行。

后台线程也叫守护线程,后台线程是依赖前台线程的,如果所有的前台线程都死了,那么后台线程自动退出(JVM就退出了),只要有一个前台线程活动,那么后台线程就活动。

2.1 前台线程

继承Thread类方式创建线程

class MyThread extends Thread {
	@Override
	public void run() {
		for (int i = 0; i < 99; i++) {
			System.out.println(i);
		}
	}
}

public class TestMyThread {
	public static void main(String[] args) {
		Thread t1 = new MyThread();
		Thread t2 = new MyThread();
		t1.start();
		t2.start();
	}
}

tips:继承的方式比较单一,因为无法再去继承其他的类。

Thread类常用方法

public void start();// 手动启动线程(并不一定立刻执行,等JVM随机分配时间片);
public final Boolean isAlive();// 测试线程是否还活着;
public static Thread currentThread();// 返回当前正在执行的线程对象的引用;
public final void setName(String name);// 设置该线程的名字
public final String getName();// 返回该线程的名称;
public final void setPriority(int newPriority);// 设置线程优先级;[MAX_PRIORITY][NORM_PRIORITY][MIN_PRIORITY]
public final int getPriority();// 获取线程优先级
public static void sleep(long millis);// 在指定的毫秒数内让当前正在执行的线程休眠;

实现Runnable接口方式创建线程

class MyRun implements Runnable {
	@Override
	public void run() {
		for (int i = 0; i < 99; i++) {
			System.out.println(i);
		}
	}
}

public class ThreadTest {
	public static void main(String[] args) {
		MyRun myRun = new MyRun();
		Thread t1 = new Thread(myRun);
		t1.start();
	}
}

Thread匿名内部类的方式创建线程

new Thread() {
    public void run() {
        for (int i = 1; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}.start();

Runnable匿名内部类的方式创建线程 - 方式1

Runnable r = new Runnable() {
    @Override
    public void run() {
        for (int i = 1; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
};
new Thread(r).start();

Runnable匿名内部类的方式创建线程 - 方式2

new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i = 1; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}).start();

2.2 后台线程

守护线程测试案例:主线程循环50次,子线程无限循环 将子线程设置成守护线程,如果主线程结束之后,子线程也结束,则子线程设置守护成功

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + "..run..");
        }
    }
});
t1.setDaemon(true);// 把t1设置成了守护线程,如果想把某一个线程设置为守护线程,必须在启动之前去设置
t1.start();// 启动t1
for (int i = 0; i <= 50; i++) {
    System.out.println(Thread.currentThread().getName() + ":" + i);
}

3. 线程同步

就比如你和赵四一起做同一套数学模拟题,同步就是你做几道,将卷子扔给赵四,赵四做几道再扔给你,循环交替,直到卷子做完。异步就是你们将卷子复制一套,然后一人做半套题,最后拼成一份答案。

异步互不干扰,资源利用率高,因为整个过程中没有人会长时间处于等待状态,但是不安全,因为有可能两个人题目刷重。
同步安全,不会刷重题目,但是效率相对而言会低一些,但有些时候,我们不得不牺牲一点效率因素,来提升安全因素。

为了共享区域的安全,我们在写程序时可以通过关键字synchronized来加保护伞。synchronized主要应用于同步代码块和同步方法中,以保证该方法在运行的时候不会被打断。又因为线程同步的实现主要是利用到了对象锁,所以我们还可以利用对象锁来实现同步;

售票系统案例

public class TicketSellTest{
	public static void main(String[] args) {
		MyThread1 mt = new MyThread1();
		Thread t1 = new Thread(mt);
		Thread t2 = new Thread(mt);
		t1.setName("窗口1");
		t2.setName("窗口2");
		t1.start();
		t2.start();
	}
}

class MyThread1 implements Runnable {
	private Integer ticketNo = 1;// 票编号

	@Override
	public void run() {
		// 为了测试明显,可以在这里睡眠1秒
		while (true) {// 循环卖票
			sell();
		}
	}

	public void sell() {// 售票方法
		if (ticketNo <= 100) {
			System.out.println(Thread.currentThread().getName() + "卖出 : " + (ticketNo++) + "号票");
		}
	}
}

tips:发现因为线程之间互相争抢资源,所以会出现两个售票点重复卖票的情况。

3.1 synchronized同步方法

我们可以利用synchronized关键字来将方法同步,此时当线程A访问这个方法的时候,其他线程将无法访问这个线程,只有当线程A访问结束之后,其他线程才能接着访问。

public synchronized void sell(){...}

3.2 synchronized同步代码块

我们还可以使用synchronized(){}块来灵活的控制我们要同步的部分。

public void sell(){
    synchronized(this){
		if (ticketNo <= 100) {
			System.out.println(Thread.currentThread().getName() + "卖出 : " + (ticketNo++) + "号票");
		}
    }
}

3.3 Lock同步

我们还可以用Lock来进行同步:在线程类中实例化锁对象,在sell方法中使用锁对象。

private Lock lock = new ReentrantLock();
...
public void sell(){
    lock.lock();// 上锁
    if (ticketNo <= 100) {
        System.out.println(Thread.currentThread().getName() + "卖出 : " + (ticketNo++) + "号票");
    }
    lock.unlock();// 解锁
}

3.4 锁类型

同步方法的原理,其实就是锁,只有多个线程使用的是同一种锁才可能发生同步现象。

synchronized(){}的小括号中可以直接看到锁的类型,而同步方法却看不到锁的类型。

3.4.1 同步实例方法的锁

测试同步方法的锁的类型,我们让线程A走同步代码块,让线程B走同步方法,如果仍旧发生同步现象,则代表同步方法中的锁和测试中同步代码块中的锁一致。

public class 测锁 {
	public static void main(String[] args) {
		Runnable r = new Ticket3();
		Thread t1 = new Thread(r);
		Thread t2 = new Thread(r);
		t1.setName("t1");
		t2.setName("t2");
		t1.start();
		t2.start();
	}
}

class Ticket3 implements Runnable {
	private int tickets = 1;
	private boolean flag = false;
	Object obj = new Object();

	@Override
	public void run() {
		while (!flag) {
			if (Thread.currentThread().getName().equals("t1")) {
				// 如果t1进来了 ,我让他走[同步块]
				synchronized (new Ticket3()) { // this锁
					if (tickets < 100)
						System.out.println(Thread.currentThread().getName() + ":" + (tickets++));
					else
						flag = true;
				}
			} else {
				// 如果t2进来了,我让他走同步方法
				sell();
			}
		}
	}

	public synchronized void sell() {
		if (tickets < 100)
			System.out.println(Thread.currentThread().getName() + ":" + (tickets++));
		else {
			flag = true;
		}
	}
}

tips:测试结果表明,同步方法的锁,就是this锁。

3.4.2 同步静态方法的锁

静态方法的锁测试原理和之前的一样。

public class 测锁 {
	public static void main(String[] args) {
		Runnable r = new Ticket3();
		Thread t1 = new Thread(r);
		Thread t2 = new Thread(r);
		t1.setName("t1");
		t2.setName("t2");
		t1.start();
		t2.start();
	}
}

class Ticket3 implements Runnable {
	private static int tickets = 1;
	private static boolean flag = false;
	Object obj = new Object();

	@Override
	public void run() {
		while (!flag) {
			if (Thread.currentThread().getName().equals("t1")) {
				// 如果t1进来了 ,我让他走[同步块]
				synchronized (Ticket3.class) { // 静态类对象锁
					if (tickets < 100)
						System.out.println(Thread.currentThread().getName() + ":" + (tickets++));
					else
						flag = true;
				}
			} else {
				// 如果t2进来了,我让他走同步方法
				sell();
			}
		}
	}

	public static synchronized void sell() {
		if (tickets < 100)
			System.out.println(Thread.currentThread().getName() + ":" + (tickets++));
		else {
			flag = true;
		}
	}
}

tips:测试结果表明,同步静态方法的锁,是所属的类对象的字节码对象锁。

3.4.3 死锁

案例:吃饭的时候,我有一根筷子,你有一根筷子,我需要你给我凑成一双,我吃饭,你需要我给你凑成一双,你吃饭,这时候就会僵持不下,发生死锁。

线程也是一样,A线程持有一个B的锁,B线程持有一个A的锁,二者谁也不肯释放锁,就会发生死锁。

死锁的现象我们应该积极避免。

死锁案例

public class 死锁 {
	public static void main(String[] args) {
		MyThread7 mt = new MyThread7();
		new Thread(mt).start();
		new Thread(mt).start();
	}
}

class MyThread7 implements Runnable {
	private Object obj1 = new Object();// obj1锁
	private Object obj2 = new Object();// obj2锁

	@Override
	public void run() {
		if (Thread.currentThread().getName().equals("Thread-0")) {
			// 线程1进来了
			synchronized (obj1) {
				System.out.println("if--obj1");
				synchronized (obj2) {
					System.out.println("if--obj2");
				}
			}
		} else {
			// 线程2进来了
			synchronized (obj2) {
				System.out.println("else--obj2");
				synchronized (obj1) {
					System.out.println("else--obj1");
				}
			}
		}
	}
}

4. 线程等待

案例:我有个资源,一个线程往这个资源里写入,另一个线程从这个资源中读出。

读写案例 - 未同步版本

public class 读写案例_未同步 {
	public static void main(String[] args) {
		Res res = new Res();// 共享资源,写在这里是为了让两个线程共享同一个res
		WriteRes in = new WriteRes(res);// 在创建Input线程类的时候将共享资源传入
		ReadRes out = new ReadRes(res);// 在创建Output线程类的时候将共享资源传入
		new Thread(in).start();// 启动一个写线程
		new Thread(out).start();// 启动一个读线程
	}
}

class Res {
	private String name;
	private String gender;
	public String getName() {return name;}
	public void setName(String name) {this.name = name;}
	public String getGender() {return gender;}
	public void setGender(String gender) {this.gender = gender;}
}

class WriteRes implements Runnable {
	private Res res;// 共享资源
	private boolean flag = false;// 中英文切换写入标志,false时写中文,true时写英文

	public Input(Res res) {// 通过构造方法获取共享资源res,并传递给当前类属性res
		this.res = res;
	}

	@Override
	public void run() {
		while (true) {// 无限写
			if (flag) {
				res.setName("zhaosi");
				res.setGender("male");
				flag = false;
			} else {
				res.setName("赵四");
				res.setGender("男");
				flag = true;
			}
		}
	}
}

class ReadRes implements Runnable {
	private Res res;// 共享资源

	public Output(Res res) {// 通过构造方法获取共享资源res,并传递给当前类属性res
		this.res = res;
	}

	@Override
	public void run() {
		while (true) {// 无限读
			System.out.print(res.getName());
			System.out.print(" ---- ");
			System.out.println(res.getGender());
		}
	}
}

案例升级:上面的代码在运行过程中会出现数据错位,比如 “赵四 — male” 或者 “zhaosi ---- 男” 的情况,如何使用同步解决这个问题。提示:让两个线程同步的前提是,两个线程可以获得相同的锁,res对象本身就是共享资源,直接拿res当锁,即可完成两个线程的同步。

读写案例 - 同步版本
在写的线程中,while循环下加synchronized (res) {}锁

while(true){
    synchronized (res) {
        // ...写的代码
    }
}

在读的线程中,while循环下加synchronized (res) {}锁

while(true){
    synchronized (res) {
        // ...读的代码
    }
}

案例再升级:将之前的读写案例,改为写一个,读一个,交替运行。

[方法提示]

wait()让某个线程等待,此时该线程会加入到线程池进行等待
notify()让某个线程被唤醒,只能唤醒一个,而且是在线程池中随机唤醒
notifyAll()唤醒线程池中的所有等待线程

tips:这三个方法是Object类中的,不是Thread类中的,而且只能在同步的前提下中使用。

思路

  • 在资源类设置一个标志变量hasRes,标识当前是否有数据,默认为false
  • 写线程中,写之前判断当前是否有数据
    如果有数据,写线程等待res.wait()
    如果没数据,执行写数据代码,并把hasRes改成true,并且唤醒读线程
  • 读线程中,读之前判断当前是否有数据
    如果有数据,执行写数据代码,并把hasRes改成false,并且唤醒写线程
    如果没数据,读线程等待res.wait()

代码:资源类添加hasRes属性和set/get方法

class Res{
    //... 其余代码略
    private boolean hasRes = false;
    
    public boolean isHasRes() { return hasRes; }
    public void setHasRes(boolean hasRes) { this.hasRes = hasRes; }
}

代码:在写线程的同步代码块中添加如下代码:

while(true){
    synchronized (res) {
        if(res.isHasRes()){// 当前有数据
            res.wait();// input等待
        }else{// 当前没数据
            // ...写数据的代码,略
            res.setHasRes(true);// 改变标志位
            res.notify();// 唤醒output
        }
    }
}

代码:在读线程的同步代码块中添加如下代码:

while(true){
    synchronized (res) {
        if(res.isHasRes()){// 有数据
            // ...读数据的代码,略
            res.setHasRes(false);// 改变标志位
            res.notify();// 唤醒input
        }else{ // 没数据
            res.wait();// output等待
        }
    }
}

tips:sleep和wait的区别

sleep()可以理解为,你上厕所上到一半,在厕所里睡着了,这时候别人想上厕所也进不来,必须等你睡醒出来或者强行叫醒你[interreput()]。
wait()可以理解为,你刚进厕所突然发现忘了带纸,只能先出去拿纸,等一会儿再来上厕所,这个时候别人可以插队进来。

区别1:这两个方法来自不同的类分别是Thread和Object。sleep是Thread的静态方法,需要当前线程来控制,而wait是作用在某个对象。
区别2:sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
区别3:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。

5. 停止线程

想停止一个线程,只能等待run方法结束(stop()方法已经过时),开启线程就一定跟循环相关,所以我们用更改变标志位的方法就可以结束循环,就可以结束run,而如果要被停止的线程处于挂起状态,则需要强行使用interrupt()方法来终止。

**下面我们在主线程中停止子线程。**
public class KillThread {
	public static void main(String[] args) {
		DeadThread deadThread = new DeadThread();// 子线程类
		Thread t1 = new Thread(deadThread);// 子线程
		t1.start();// 启动子线程
		for (int i = 0; i < 10; i++) {// 主线程循环10次
			System.out.println("主线程运行:" + i);
		}
		System.out.println("主线程改变子线程循环标志位");
		deadThread.setDead(true);// 改变子线程循环标志位true
	}
}

class DeadThread implements Runnable {
	private boolean dead = false;// 线程体循环标志位

	@Override
	public void run() {
		while (!dead) {
			System.out.println("子线程运行..");
		}
		System.out.println("子线程结束..");
	}
	public boolean isDead() {return dead;}
	public void setDead(boolean dead) {this.dead = dead;}
}

问题:当线程体wait或者sleep时,处于挂起状态,这时候的线程仍然存活,但是标志位的方法已经不能用了。
解决:使用interrupt方法清除挂起状态(会抛异常,可以在异常处理中更改标识符结束循环)。

步骤1:将上面案例中的deadThread.setDead(true);变成t1.interrupt();

//deadThread.setDead(true);// 改变子线程循环标志位true
t1.interrupt();// 清除子线程挂起状态,并打断

步骤2:线程体中添加synchronized并改写如下代码:

@Override
public synchronized void run() {// wait必须在同步方法中才能使用
    while (!dead) {
        try {
            wait();// 线程挂起
        } catch (InterruptedException e) {
            dead = true;// 爆发异常后,改变标志位,否则子程序还是不结束
        }
        System.out.println("子线程运行..");
    }
    System.out.println("子线程结束..");
}

6. 线程插队让步

当A线程读到了B线程的join()方法,会停下来,等B死掉后,再执行,join一般用于临时加入一个线程方法,yeild()用于让出一个时间片(不明显),就比如食堂排队打饭,我调用join方法可以插队买饭,而我调用yield方法是让给你一次机会,让你排在我的前面买饭。

join()方法写在哪个线程中,就插队哪个线程。

join()方法如果写在start()之前,不报错,但是是没有插队效果的。

yield方法是静态的,使用的时候请使用Thread.yield();

public class 线程插队{
	public static void main(String[] args) throws InterruptedException {
		JoinThread joinThread = new JoinThread();
		Thread t1 = new Thread(joinThread);
		Thread t2 = new Thread(joinThread);
		t1.start();// t1插队前需要先启动,否则无法完成插队效果
		t1.join();// t1插队,此时主线程会挂起,直到t1死去才继续运行
		t2.start();// 主线程被t1插队,所以t1执行完之前,t2不会被启动

		for (int i = 0; i < 100; i++) {// 主线程
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}
class JoinThread implements Runnable{
	@Override
	public void run() {
		for(int i=0;i<10;i++) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}

7. 线程调度类

java.util.Timer 和 java.util.TimerTask这两个类可以负责java中的定时和调度相关内容。

Timer中的schedule(TimerTask task,long delay,long period)方法负责定时,读音[si gai zhu ler],参数1是你的任务类,参数2是几毫秒后执行,参数3是周期,每隔多少毫秒执行一次。

Timer中的cancel()方法负责结束定时任务。

案例:设计我的闹钟程序,每天早上6点,闹钟响起,控制台输入"q"结束闹钟。

public class 我的闹钟{
	public static void main(String[] args) throws IOException {
		Timer timer = new Timer();// 定时对象
		MyTask myTask = new MyTask();// 定时任务
		timer.schedule(myTask, 0, 1000);// 0毫秒后,每隔1000毫秒执行一次myTask
		if (System.in.read() == 'q') {
			// timer.cancel(); // 关闭schedule
			myTask.setFlag(false);// 闹钟设置为不响
		}
	}
}

class MyTask extends TimerTask {
	private boolean flag = false;// 闹钟标志位 - 不响
	@Override
	public void run() {
		String dateStr = new SimpleDateFormat("hhmmss").format(new Date());// 获取系统时间格式化成时分秒
		// System.out.println(dateStr);
		if ("060000".equals(dateStr)) {// 每日早晨6点
			flag = true;// 闹钟标志位 - 响
		}
		if (flag) {
			System.out.println("起床了.....");
		}
	}
	public boolean isFlag() {return flag;}
	public void setFlag(boolean flag) {this.flag = flag;}
}
  1. 线程的生命周期
    在这里插入图片描述

tips:线程总是处于6种生命周期之一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值