目录
什么是进程?
简单的说就是一个正在运行的程序实例(Windows系统中,每个运行的 Java 程序都是一个独立的进程)。
程序的一次执行。由操作系统创建并分配资源,执行一个单独的任务。
进程是系统进行资源分配和调度的独立单位,每个进程都有自己的内存空间和系统资源。进程内所有线程共享堆存储空间,保存程序中定义的对象和常量池。
什么是线程?
进程内的执行单元,不分配单独的资源,执行一个单独的子任务。
线程是进程内调度和分派的基本单位,共享进程资源。每个线程有自己的独立的栈存储空间,保存线程执行的方法以及基本类型的数据。
运行的 Java 程序内含至少一个主线程 main ,用户可以在 Java 程序中自定义并调用多个线程。JVM 垃圾回收线程也是一个独立的线程。
进程和线程区别联与系是什么?
(1)进程包含至少一个线程,如果只有一个线程,这个线程通常叫做主线程。线程会去执行进程的一部分代码或者指令
(2)进程有独立的内存存储,线程共享进程的内存空间,此外线程有自己独立的的栈存储。
(3)进程通信只能依靠pipe管道或者socket,一个进程内的线程是可以直接通信的
线程示例:
如在java的程序里面每启动一个main方法,其实就是启动了一个jvm进程,而main方法就是我们上面所说的进程中至少包含一个线程的主线程。
* 创建线程方式一:继承Thread类
* 1,创建一个继承于Thread类的子类
* 2,重写Thread类中的run()方法,并将此线程执行的操作声明在run()中
* 3,创建Thread类的子类对象
* 4,通过此对象调用start()方法;
* 如果这个类我们设置之后只创建一个对象,再不用,我们可以使用thread的匿名子类
在Java中java.lang.Thread 这个类表示线程,一个类可以继承Thread并重写run方法来实现一个线程,代码如下:
public class Demo001 extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
* 创建线程方式二:实现Runnable接口
* 1.创建一个实现了Runnable接口的类
* 2.实现类去实现Runnable中的抽象方法:run();
* 3.创建实现类对象
* 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象。
* 5.通过Thread类的对象调用start()方法、
通过继承Thread来实现线程虽然比较简单,但Java中只支持单继承,每个类最多只能有一个父类,如果类已经有父类了,就不能再继承Thread,这时,可以通过实现java.lang.Runnable接口来实现线程。Runnable接口的定义很简单,只有一个run方法,如下所示:
public class Demo001 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
无论是通过继承Thead还是实现Runnable接口来创建线程,启动线程都是调用start方法,
还需要创建一个Thread对象来调用Start方法。;
两种方式比较;
开发中,我们优先选择实现Runnable接口的方式。
原因:1,实现的方式没有类的单继承性的局限性。 2,实现的方式更适合来处理多个线程有共有的数据情况。
联系:Thread类也是实现了Runnable接口,Thread中的run也是重写Runnable接口中的run()方法;
相同点:两种方式都需要重写run方法,将线程要执行的操作放在run方法中;
* 新增创建线程方式一:实现Callable接口
操作流程:
(1)创建Callable实现类的实例,并实现call方法(将此线程需要执行的操作声明在call()中)
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象
(3)使用FutureTask对象作为Thread对象的target创建、并启动新线程
(4)调用FutureTask对象的get方法来获取子线程执行结束后的返回值
//1.创建一个实现Callable接口的实现类
class NumThread implements Callable {
// 2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread()+""+i);
sum += i;
}
}
return sum;
}
}
public class ThreadTest3 {
public static void main(String[] args) throws Exception {
//3.创建Callable接口实现类对象
NumThread numThread=new NumThread();
//4. 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用Start()
new Thread(futureTask).start();
try {//6.获取Callable中的call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
***与使用Runnable相比,Callable功能更加强大些
- 相比run方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask 类,比如获取返回值结果。
Java创建线程 为什么Callable需要FutureTask包装一下?
(1)在并发编程时,一般使用实现Runnable接口的实例传递给Thread类的参数来创建线程,然后给线程池,这种情况下是单线程不需要线程的结果。所以run()的返回值是void类型。如果是一个多线程协作程序,当使用多线程来计算,如果有其中一个线程的下一步操作正好需要另外一个线程的返回值时(这里体现了多线程协作程序中的协作两个字),后者就需要实现callable接口了,因为实现Callable接口了。
(2)线程是属于异步计算模型,所以你不可能直接从别的线程中得到函数返回值。这时候Future就出场。Future可以监视目标线程调用call的情况,当你调用Future的get()方法以获得结果时,当前线程就开始阻塞,直接call方法结束返回结果。
查看Thread类的构造方法
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
从该构造方法可以看出,参数是一个Runnable,那么可以分析得到:
首先可以通过FutureTask的构造方法,传入Callable接口的实例,构造FutureTask对象
由于FutureTask间接实现了Runnable接口,同时Thread类的构造方法要求放入一个Runnable,这时候就可以放入当前构造的FutureTask对象
最后通过Thread的start()方法开启一个线程
Future Callable FuterTask关系图
* 新增创建线程方式二:使用线程池
使用格式:
*1.创建一个实现了Callable或Runnable的实现类
*2.提供指定线程数量的线程池
*3.执行指定的线程的操作。需要提供实现Runnable或Callable接口实现类的对象
*4.关闭连接池
//创建一个实现了Callable或Runnable的实现类,并重写接口中的方法
class NumberThread implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread()+":"+i);
}
}
}
}
class NumberThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread()+":"+i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1= (ThreadPoolExecutor) service;
//设置线程池的属性
System.out.println(service.getClass());//getClass()声明在object类中
//执行指定的线程的操作。需要提供实现Runnable或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit();//适合适用于Callable
//关闭连接池
service.shutdown();
}
}
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
>提高响应速度(当任务到达时,任务可以不需要等到线程创建就能立即执行。)
>降低资源消耗(通过重复利用已创建的线程降低线程创建和销毁造成的消耗)
>便于线程管理(线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。)
corePoolSize: 核心池的大小
maximunPoolSize: 最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程池相关API
- JDK 5.0起提供了线程池相关API:ExecutorService 和Executors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
> void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
> <T> Future<T> submit(Callable<T>task):执行任务,有返回值,一般又来执行Callable
> void shutdown():关闭连接池
* Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
> Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
> Executors.newFixedThreadPool(n);创建一个可重用固定线程数的线程池
> Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
> Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
线程类中常用方法
start(); |
|
run(); | 需要重写Thread类中的run方法,将创建此线程需要执行的操作,放在重写之后的run方法内部; |
currentThread(); | 静态方法。返回执行当前代码的线程 |
getName(); | 获取当前线程的名字 |
setName(); | 设置当前线程的名字 |
yield(); | 释放当前线程(CPU)的执行权;当然下次也有可能将执行权依旧分配给原先的线程; |
join(); | 在线程A中调用线程B的join(),此时线程A进入阻塞状态,直到线程B完全执行完以后,A才结束阻塞状态,就看CPU给 * A线程什么时候分配到执行权它什么时候就执行; * 理解:A线程执行到某个地方需要B线程提供的数据,这时就需要B数据的线程调用join方法; * 直到B线程完全执行完,才会执行A线程 |
stop() | 已过时,结束当前线程 |
sleep(long millitime) | 静态方法 使线程强制阻塞millitime毫秒数,在指定时间内当前线程是阻塞状态* 之后就看CPU给该线程什么时候分配到执行权它什么时候就执行; 例子:倒计时 |
isAlive() | 判断线程是否存活; |
join 方法底层剖析:
其底层通过调用 wait 方法实现。当前线程执行 方法,线程会停止运行并释放对象锁,同时线程进入线程 t 对象的等待池,直到被唤醒进入就绪状态。通常用于主线程 main,等到线程 t 终止时自动被唤醒。调用 join 方法需要捕获或抛出 InterruptException 异常。
t.join()
同样允许计时等待。当前线程执行join 方法,计时结束后线程会被自动唤醒进入就绪状态,无须等待子线程结束时唤醒。
t.join(1000)
wait和notify剖析:
Object 类定义了 wait 和 notify 方法,通常被用于线程间交互/通信。必须在同步环境下(synchronized)使用,否则会抛出 非法监视器状态异常。假定 obj 为同步环境上锁的对象:
当前线程执行wait 方法,线程会停止运行并释放对象锁 obj,其他线程可以访问其资源。同时线程进入 obj 对象的等待池,直到被 notify 方法唤醒进入就绪状态。调用 wait 方法需要捕获或抛出 InterruptException 异常。
obj.wait()
wait 方法允许计时等待。当前线程执行 方法,计时结束后线程会被自动唤醒进入就绪状态。
obj.wait(1000)
线程生命周期
线程安全问题
注意:有共享数据之后才出现了线程安全问题:
1.什么是线程安全问题
就是在多线程环境中 , 存在多个线程共享同一个数据 , 一个线程正在访问的共享数据被其他线程修改了, 那么就发生了线程安全问题。
2.为什么会有线程安全问题?
当多个线程同时共享一个全局变量,或者静态变量, 进行写的操作时, 可能会发生数据的冲突问题 ,也就是线程安全问题, 但是做读的操作不会引发线程安全问题。
线程安全:指多个线程在执行同一段代码的时候采用加锁机制, 使每次的执行结果和单线程执行结果一样的, 不存在执行程序时出现意外结果
线程不安全:线程不安全是指不提供加锁机制保护, 有可能出现多个线程先后更改数据造成所得到的数据是脏数据
3.如何解决线程安全问题
例子:创建三个窗口卖票,总票数为100张.使用实现Runnable接口的方式
- .问题:卖票过程中,出现了重票、错票-->出现了线程的安全问题
②.问题出现的原因:当某个线程操作共享数据(车票)的过程中,尚未操作完成时,其他线程参与进来,也同时操做共享数据(车票)。
③.如何解决:当一个线程a在操作共享数据(车票ticket)的时候,其他线程不能参与进来。直到线程a操作完共享数据(车票ticket)之后,所有线程在重新争抢CPU的执行权,谁抢到谁再操作共享数据。这种情况即使线程出现了阻塞,也不会出现多个线程同时出售同一张票。
④.在Java中,我们通过同步机制,来解决线程的安全问题。
方式一:同步代码块
synchronized( 同步监视器){
//需要被同步的代码
}
说明:1.操作共享数据的代码,即为需要被同步的代码;要包的不多不少
2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁,我们常用object对象。
要求:多个线程必须要共用同一把锁。
补充:根据实际条件我们可以用this充当锁(继承Thread实现多线程时)我们也可以用类充当监视器,类也是对象。
方式二:同步方法
如果操作共享数据的代码我们封装在一个方法内,我们不妨将此方法声明为同步的(使用synchronized修饰方法);
也有锁,锁是this;
方式三:lock锁
(JDK5.0新增);
- 实现线程(实现Runnable)。
- 实例化Reentrantlock;
- 手动调用锁定方法lock();
- 手动调用解锁方法unlock();
下面我们演示一个案例,调用三个线程来出售100张电影票:
public class MyRunnable implements Runnable {
private int tickets = 100;
private Object obj = new Object();
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "开始售第:" + tickets + "张票");
tickets--;
} else {
break;
}
} finally {
lock.unlock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class MyRunnableDemo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr,"第一窗口");
Thread t2 = new Thread(mr,"第二窗口");
Thread t3 = new Thread(mr,"第三窗口");
t1.start();
t2.start();
t3.start();
}
}
输出结果(由于结果篇幅过长,这里仅展示部分):
线程通信
(实现线程的交替)
关键方法:
wait(); | 使得线程阻塞,直到调用方法被唤醒,执行wait方法后会释放锁,和sleep方法不同,执行sleep方法后,不会释放锁; |
notify(); | 唤醒单个线程,看优先级 |
notifyAll(); | 唤醒多个线程, |
1,说明:以上三个方法,必须使用在同步代码块或同步方法中,注意不包括lock锁中;
2,这三个方法的调用者必须是同步代码快或者同步方法中的同步监视器(锁),否则会出现报错,illegalMonitorStateException异常;
3,这三个方法定义在object类中,便于任一锁都可以调用。
面试题:synchronized和lock的异同;
相同:二者都可以解决线程安全问题
不相同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器,lock需要手动的启动同步(lock),同时结束同步也需要手动的实现(unlock);
优先使用顺序:
Lock——同步代码块(已经进入了方法体,分配了相应资源)——同步方法(在方法体之外)
面试题:如何解决线程安全问题?有几种方式?
面试题:sleep和wait方法的异同;
相同点:一旦执行方法,都可以是的当前的线程进入阻塞状态;
不同点;①方法声明、定义的位置不一样,thread类声明sleep(),object中声明wait();
- 调用的范围不同;sleep()可以再任一位置调用,而wait()只能在同步代码快、方法中调用;
- 关于是否释放同步监视器;执行到sleep()不会释放,而执行到wait()会释放锁
总结同步的方式:
优点:解决了线程的安全问题。
缺点:操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
4.什么是线程同步
当有一个线程在对内存进行操作时.其他线程都不可以对这个内存地址进行操作,其他线程才能对该内存地址进行操作, 而其他线程又处于等待状态。
理解共享 只有共享资源的读写才需要同步, 如果不是共享资源 那么就不需要同步了
线程同步其实 实现的是线程排队
防止线程同步访问共享资源造成冲突
变量需要同步 , 常量不需要同步(常量存在方法区)
多个线程访问共享资源的代码有可能是同一份代码 也有可能是不同一份代码, 无论是否执行同一份代码 只要这些线程的代码访问同一份可变的共享资源, 这些线程之间就需要同步