线程依赖于进程而存在。
进程
- 通过任务管理器看到进程的存在。
- 只有运行的程序才会出现进程。
- 就是正在运行的程序。是系统进行资源分配和调用的独立单位。
- 每一个进程都有它自己的
内存空间和系统资源。
多进程有什么意义?
单进程的计算机只能做一件事情,现在的计算机可以做多件事情。
比如:一边玩游戏(游戏进程),一边听音乐(音乐进程)。
现在的计算机都支持多进程,可以在一个时间段内执行多个任务,提高CPU的使用率。
问题:一边玩游戏一边听音乐是同时进行的吗?
不是,单核CPU在某一时间点上只能做一件事情。只是CPU做着程序间的高效切换让我们觉得是同时进行的。
线程
- 在同一个进程内又可以执行多个任务。每一个任务是一个线程。
- 线程是程序的执行单元,执行路径。是程序使用CPU的最基本单位。
- 单线程:程序只有一条执行路径。
- 多线程:程序有多条执行路径。
多线程有什么意义?
多线程的存在,不是提高程序的执行速度,而是为了提高应用程序的使用率。
程序的执行其实都是在抢CPU的资源,CPU的执行权。
多个进程实在抢这个资源,某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。
不敢保证哪个线程能够在哪个时刻抢到,所以线程的执行有随机性。
比如:扫雷程序(一个计时器,一个鼠标点击);迅雷下载
线程两种调度模型
- 分时调度模型 所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。
- 抢占式调度模型 优先让优先级高的线程使用CPU,如果线程的优先级相同,那么随机选择一个,优先级高的线程获取的CPU时间片相对多一些。
使用t1.getPriority()查看优先级
使用t1.setPriority(…)设置优先级
知识点
休眠线程(隔一段时间运行一次,sleep);
加入线程(运行完该线程才去运行其他线程,join);
礼让线程(使得运行次序尽量交替进行,yield)
守护线程(当一个线程结束之后,其他线程也将不在运行,典型例子:坦克大战 setDaemon)
中断线程(超过一段时间可以将其停止,interrupt)
线程的生命周期:(记住步骤)
新建:创建线程对象
就绪:有执行资格,没有执行权
运行:有执行资格,有执行权
(阻塞):由于一些操作让线程处于该状态,没有执行资格,没有执行权,另一些操作可以把它激活,激活后处于就绪状态。
死亡:线程对象变成垃圾,等待被回收
判断一个程序是否会有线程安全问题的标准
1.是否有多线程环境
2.是否有共享数据
3.是否有多条语句操作共享数据**
解决方法:因为12点改变不了,我们只能对3这一点进行修改
法1:同步代码块,对象obj作为一把锁的功能。
private Object obj = new Object();
public void run() {
synchronized (obj) { //也可用this,如果是静态的方法,则是类的字节码文件对象,
......
}
}
法2:把同步关键字加在方法上。
public void run() {
sellTicket();
}
private (static) synchronized void sellTicket(){ //默认锁对象是this,静态时默认为类.class
......
}
法3:Lock接口,ReentrantLock是Lock的实现类。
private Lock lock = new ReentrantLock();
public void run(){
...
try{
lock.lock();
//需要加锁的代码
...
}finally{
lock.unlock();
}
}
并发、并行、同步
- 并发:逻辑上同时发生,指在某一时间段内同时运行多个程序。
- 并行:物理上同时发生,指在某一时间点同时运行多个程序。
- 同步:特点:多个线程使用的是同一个锁对象。同步的出现解决了多线程的安全问题。当线程相当多时,因为每个线程都会判断同步上的锁,这是很耗费资源的,无形中降低程序的运行效率。
Java程序的运行原理
由java命令启动JVM,JVM启动相当于启动了一个进程。
由该进程创建一个主线程去调用main方法。
JVM虚拟机启动是多线程的。原因是垃圾回收线程也要先启动,否则很容易出现内存溢出。
如何实现多线程
由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。
Java不能直接调用系统功能,所以没有办法直接实现多线程程序。
但是,java可以调用C/C+ +写好的程序来实现多线程程序。
由C/C++去调用系统功能创建进程,然后由Java提供一些类(Tread)去调用线程,就实现了多线程程序。
查看API,发现有两种方式实现多线程程序。
方式一:继承Thread类
重写run方法,用来包含那些被线程执行的代码。
run方法的调用其实就是普通方法的调用,看到的是单线程的效果。
start方法,首先启动了线程,然后再由jvm去调用该线程的run方法。
package thread;
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(getName() + "---" + i);
}
}
}
/////////////////////////////////////////
package thread;
public class ThreadDemo {
public static void main(String[] args) {
MyThread m1 = new MyThread();
MyThread m2 = new MyThread();
m1.start();
m2.start();
}
}
结果是两个线程抢资源,200个数字交替打印出。
方式二:实现Runnable接口(常用)
1. 自定义类MyRunnable实现Runnable接口
2. 重写run方法
3. 创建MyRunnable类的对象
4. 创建Thread类的对象,并把3步骤的对象作为构造参数传递
package thread;
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
////////////////////////////////////////////////
package thread;
public class MyRunnableDemo {
public static void main(String[] args) {
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.start();
t2.start();
}
}
方式三:实现Callable接口
需要用线程池配合
方式四:匿名内部类方法创建并开启线程
public class Demo {
public static void main(String[] args) {
new Thread() {
// 重写run方法
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":"+ i);
}
};
}.start();
//
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":"+ i);
}
}
}) {
}.start();
}
}
死锁问题
同步弊端:效率低,如果出现了同步嵌套,就容易产生死锁问题。
死锁:是指两个或两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象。
线程间通信 生产者消费者问题
不同种类的线程间针对同一个资源的操作。
例子:学生
资源类:Student
设置学生数据:SetThread(生产者)
获取学生数据:GetThread(消费者)
测试类:StudentDemo
正常思路:
A:生产者 先看是否有数据,有就等待,没有就生产,生产完之后通知消费者来消费
B:消费者 先看是否有数据,有就消费,没有就等待。通知生产者生产数据
为了处理这样的问题,Java就提供了一种机制,等待唤醒机制 。
等待唤醒:
Object类中提供了三个方法:
wait():等待
notify():唤醒单个线程
notifyAll():唤醒所有线程
这些方法的调用必须通过锁对象调用,而使用的锁对象是任意锁对象。所以,必须定义在Object类中。
*注:
1wait之后就释放锁,将来唤醒的时候从这里开始执行
2唤醒并不表示有执行权,必须还是去抢执行权*
例子代码如下:
//Student类/
public class Student {
String name;
int age;
boolean flag;// 默认情况下没有数据,如果为true,说明有数据
}
//设置学生数据线程类//
public class SetThread implements Runnable {
private Student s;
private int x = 0;
public SetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
// 判断有没有数据
if (s.flag) {
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (x % 2 == 0) {
s.name = "james";
s.age = 32;
} else {
s.name = "kobe";
s.age = 37;
}
x++;
// 修改标记
s.flag = true;
// 唤醒线程
s.notify();
}
}
}
}
//读取学生数据线程类
public class GetThread implements Runnable {
private Student s;
public GetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if (!s.flag) {
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(s.name + ":" + s.age);
// 修改标记
s.flag = false;
// 唤醒线程
s.notify();
}
}
}
}
//测试结果类///
public class StudentDemo {
public static void main(String[] args) {
Student s = new Student();
SetThread st = new SetThread(s);
GetThread gt = new GetThread(s);
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);
t2.start();
t1.start();
}
}
新的线程生命周期图如下:
常见的情况:
A:新建–就绪–运行–死亡
B:新建–就绪–运行–就绪–运行–死亡
C:新建–就绪–运行–其他阻塞–就绪–运行–死亡
D:新建–就绪–运行–同步阻塞–就绪–运行–死亡
E:新建–就绪–运行–等待阻塞–同步阻塞–就绪–运行–死亡
线程组
Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。
//线程默认情况下属于main线程组
Thread.currentThread().getThreadGroup().getName();
修改线程组:创建一个线程组,创建其他线程的时候,把其他线程的组指定为我们自己新建线程组
ThreadGroup tq = new ThreadGroup("这是一个新的组");
MyRunnable my = new MyRunable();
Thread t1 = new Thread(tg,my,"james");
tg.setDaemon(true);//表示该组线程都是守护线程,好处可以对一个组的线程进行统一控制操作,但较少使用
线程池
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要使用大量生命周期很短的线程时,应该考虑使用线程池。
- 线程池中的每个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
- 在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,java内置支持线程池。(Executors工厂类)
如何实现线程池代码?
A:创建一个线程池对象,控制要创建几个线程对象
public static ExecutorService newFixedThreadPool(int nThreads);
B:这种线程池的线程可以执行:
可以执行Runnable对象或者Callable对象代表的线程,做一个类可以实现Runnable接口
C:调用如下方法即可
Future<?> submit(Runnable task)
<T> Future<T> submit(Callable<T> task)
D:结束线程池
代码如下:
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
///////////////////////////////////////
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorsDemo {
public static void main(String[] args) {
// 创建一个线程池对象,控制要创建几个线程对象。
ExecutorService pool = Executors.newFixedThreadPool(2);
// 可以执行Runnable对象或者Callable对象代表的线程
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
// 结束线程池
pool.shutdown();
}
}
定时器
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。java中,可以用过Timer和TimerTask类来实现定义调度的功能。开发中使用Quartz,一个java编写的开源调度框架。
import java.util.Timer;
import java.util.TimerTask;
public class TimerDemo {
public static void main(String[] args) {
// 创建定时器对象
Timer t = new Timer();
// 3秒后执行爆炸任务
t.schedule(new MyTask(t), 3000);
}
}
// 做一个任务
class MyTask extends TimerTask {
private Timer t;
public MyTask() {
}
public MyTask(Timer t) {
super();
this.t = t;
}
@Override
public void run() {
System.out.println("boom");
t.cancel();
}
}
思考题
1.多线程有几种实现方案,分别是哪几种?
两种。
继承Thread类
实现Runnable接口
扩展一种:实现Callable接口。这个和线程池结合。
2.同步有几种方式,分别是什么?
两种。同步代码块和同步方法。
3.启动一个线程使用run()还是start(),有什么区别
start()
run():封装了被线程执行的代码,直接调用仅仅是普通方法的调用。
start():启动线程,并由JVM自动调用run()方法。
4.sleep()和wait()方法的区别
sleep:必须指定时间;不释放锁
wait:可以指定也可以不指定时间;释放锁
5.为什么wait(),notify(),notifyAll()等方法都定义在Object类中
因为这些方法的调用时依赖于锁对象的,而同步代码块的锁对象是任意锁。而Object代表任意的对象,所以定义在这里。
6.线程的生命周期图