1. 基本概念介绍
-
程序:为了解决某个特定的问题,使用某种语言编写的指令的集合。即:一段静态代码、静态对象。
-
进程:程序的一次执行过程。或者正在运行的一个程序。是一个动态的过程:程序的执行过程有自己的声明周期。
-
线程:进程可以细化为多个线程,是程序内部的一条执行路径。
3.1 一个进程同一时间可以并行多个线程,每个线程拥有自己的一套资源(虚拟机栈和程序计数器),多个线程共享同一个进程的资源(堆空间和方法区)。
3.2 线城市调度和执行的单元,每个线程拥有独立的虚拟机栈和程序计数器,线程切换相对进程而言开销小。 -
单核CPU和多核CPU
单核CPU:假的多线程,因为单位时间只能有一个线程执行。只不过CPU短时间内在多个线程间切换表现出来多个线程都在执行。多核CPU:类似多个车道多辆车同时前进,是真正的多个线程同时被执行。
一个Java应用程序执行过程,至少启动三个线程:主线程、GC垃圾回收守护线程、异常处理线程。
-
并行和并发
并行:多个CPU同时执行多个任务。比如:多个车道多辆车同时前行。
并发:一个CPU在一段时间执行多个任务,这单位时间内,只能执行一个任务。
2. 多线程的使用和创建
-
多线程的优点:
1.1 提高应用程序的响应时间,对图形化界面更有意义,可增强用户体验。
1.2 提高CPU的利用率。
1.3 改善程序结构。将既长又复杂的进程分为多个线程、独立运行,利于修改和理解。 -
何时需要创建多线程?
2.1 程序中需要同时执行多个任务时。
2.2 程序需要实现一些等待的任务时,用户输入、文件读取、网络操作、搜索等。
2.3 需要一些后台运行的程序时。
2.1 线程的创建和使用
- 方式一:继承Thread类
- 方式二:实现Runnable接口
- 方式三:实现Callable接口
- 方式四:使用线程池
2.1.1 Thread类
2.1.1.1 构造函数
Thread()
:创建新的Thread对象。Thread(String threadname)
:创建指定线程名的Thread对象。Thread(Runnabletarget)
:指定创建线程的目标对象,它实现了Runnable接口中的run方法。
2.1.1.2 编写多线程的代码
步骤:
- 创建Thread类的子类。
- 重写run()方法,将线程需要执行的代码放在run()方法中。run()方法没有返回值、不能抛出异常。
- 实例化创建的子类对象。
- 子类对象调用start()方法。
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); ////启动当前线程;调用当前线程的run()方法
//问题一:不能通过直接调用run()的方式启动线程,
//myThread.run(); //这个仅仅是在main线程中创建对象调用方法,没有创建线程的概念
//问题二:在启动一个线程,遍历100以内的偶数,不可以让已经start()的线程再去执行start()
// 会报IllegalThreadStateException
//myThread.start();
MyThread myThread1 = new MyThread();
myThread1.start();
for (int i = 0; i < 100; i++) {
System.out.println(i + "***********");
}
}
}
//1. 创建一个继承于Thread类的子类
class MyThread extends Thread {
// 2. 重写Thread类的run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
2.1.1.3 Thread类中定义的方法
-
start() : 启动当前线程,调用当前线程的run()方法。
-
run() : 重写Thread类中的run()方法,将线程需要执行的代码放在run()中。
-
currentThread(): 静态方法,通常使用Thread.currentThread() 方式返回当前线程对象
-
sleep(long millitime) : 让当前线程“睡眠”指定的时间。线程进入阻塞状态,当“睡眠”结束之后,线程再次进入就绪状态。
-
getName() : 获取当前线程的名字
-
setName() : 设置当前线程的名字
-
yield() : 当前线程放弃CPU的控制权。
-
join() : 在线程a中调用线程b.join(), 此时线程a进入阻塞状态,直到线程b完全执行完以后,线程a结束阻塞。
-
stop() : 过时不使用。强制结束当前线程的声明周期。
-
isAlive() : 判断当前线程是否保持活性
2.1.1.3 Thread类中设置线程的优先级
优先级常量:
MAX_PRIORITY
:10MIN _PRIORITY
:1NORM_PRIORITY
:5 —> 默认优先级
线程优先级的相关的方法:
getPriority()
: 返回当前线程的优先级setPriority()
: 改变当前线程的优先级
class NumberThread extends Thread{
public void run() {
sout("hello");
}
}
NumberThread numberThread = new NumberThread();
numberThread.setPriority(NORM_PRIORITY);
注意:
- 高优先级的线程可能抢占低优先级线程的执行权。但是这仅仅是从概率上讲,高优先级的线程有更大的可能性会优先执行。但这并不意味着只有高优先的线程执行完之后,低优先级的线程才能被执行。
2.1.2 实现Runnable接口
2.1.2.1 编写多线程的代码
步骤:
- 创建一个实现了Runnable接口的实现类
- 实现接口中的run()方法
- 创建实现类子类的对象
- 创建Thread类的对象,将实现类子类的对象作为参数传入到Thread(实现类子类对象)。
- 通过Thread类的对象调用start(),启动当前线程,调用run()方法。
class MThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public static void main(String[] args) {
Window2 window2 = new Window2();
Thread t1 = new Thread(window2);
Thread t2 = new Thread(window2);
Thread t3 = new Thread(window2);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
2.1.2.2 继承Thread方式和实现Runnable方式的联系与区别
1. 开发中优先选择:实现Runnable接口的方式
2. 原因 :
2.1 实现的方式没有单继承性的局限性;
2.2 实现的方法更适合来处理多个线程有共享数据的情况。
联系:Thread类实现了Runnable接口
相同点:两种方式都需要重新run()方法,将线程需要执行的代码放在run()中。
2.1.3 实现Callable接口
2.1.3.1 编写多线程的代码
步骤:
- 创建一个实现了Callable接口的实现类;
- 实现接口中的call()方法,将此线程需要执行的小左声明在call()中;
- 创建Callable接口实现类子类的对象
- 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象看,并调用start()
- 获取Callable中call()方法的返回值。
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for(int i = 1;i <= 100;i++){
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//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();
}
}
}
Future
接口
- 可以对具体
Runnable
、Callable
任务的执行结果进行取消、查询是否完成、获取结果等;FutrueTask
是Futrue
接口的唯一的实现类;FutureTask
同时实现了Runnable
,Future
接口。它既可以作为Runnable
被线程执行,又可以作为Future
得到Callable
的返回值;
2.1.4 使用线程池
- 使用线程池的原因:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程、对性能影响很大。
- 思路:提前创建好多个线程,放入线程池,使用时直接获取,使用完放回线程池中。可以避免频繁创建和销毁线程对象浪费资源,实现重复利用。
- 好处:
3.1 提高相应速度(减少了创建新线程的时间)
3.2 降低资源消耗(重复利用线程池中,线程,不需要每次创建)
3.3 便于管理,可以使用的线程池管理方法
setCorePoolSize():核心池的大小
setMaximumPoolSize(): 最大线程池
setKeepAliveTime(long time, TimeUnit unit) : 线程终止执行之后保存活性的时间
2.1.4.1 编写多线程的代码
步骤:
- 提供指定线程数量的线程池;
- 执行指定的线程操作。需要提供实现Runnable接口或者Callable接口实现类的对象
- 关闭连接池
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread()); //适合适用于Runable
service.execute(new NumberThread1()); //适合适用于Runable
// service.submit(Callable callable); //适合适用于Callable
//3.关闭连接池
service.shutdown();
}
}
2.2 线程间的同步
问题:
- 多个线程执行的不确定性引起执行结果的不稳定。
- 多个线程对数据的共享,会造成数据的不完整性。
解决方案:
通过线程同步的方式来解决多线程中出现的不稳定性、数据不完整的问题。设置为具有同步的代码同一时刻只能有一个线程去执行,其他线程等待,直到该线程把同步的代码执行结束。
同步的方式三种:
- 同步代码块
- 不同方法
- lock()/unlock()
2.2.1 同步代码块
格式:
synchronized(同步监听器) {
//需要被同步的代码
}
说明:
1. 操作共享数据的代码,即为需要被同步的代码 --> 不能包含过多,也不能包含少了。过多会导致逻辑错误,过少没有达到同步的效果。
2. 共享数据:多个线程共同要操作的数据。
3. 同步监视器:必须是一个类的对象来充当。
切记切记切记:多个线程要同步一个同步监听器,否则达不到同步的效果。
class Windows1 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
synchronized (this) {//此时的this:唯一的windows1的对象 切记要是多个对象所共享的一个对象
if (ticket > 0) {
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为: " + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowsTest1 {
public static void main(String[] args) {
Windows1 w = new Windows1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t1.start();
t2.start();
}
}
2.2.2 同步方法
同步方法的要点:
1. 同步方法仍然涉及到同步监视器,只不过不需要们显式的声明。
2. 非静态的不同方法,同步监视器是 (this)
静态的方法,同步监视器是:当前类的类对象 (类名.class)
实现Runnable的代码
class Windows3 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
public synchronized void show() { //同步监视器:this
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为: " + ticket);
ticket--;
}
}
}
public class WindowsTest3 {
public static void main(String[] args) {
Windows3 w3 = new Windows3();
Thread t1 = new Thread(w3);
Thread t2 = new Thread(w3);
t1.setName("窗口1");
t2.setName("窗口2");
t1.start();
t2.start();
}
}
继承Thread的代码
class Windows extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private static synchronized void show(){//同步监视器:Window4.class
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
public class WindowsTest {
public static void main(String[] args) {
Windows t1 = new Windows();
Windows t2 = new Windows();
t1.setName("窗口1");
t2.setName("窗口2");
t1.start();
t2.start();
}
}
2.2.3 Lock锁方式解决线程安全问题
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。ReentrantLock
类实现了Lock
,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以显式加锁、释放锁。- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用
Lock
对象充当。
class Windows implements Runnable{
private int ticket = 100;
//1.实例化ReentrantLock 线程共享的对象
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try
//2. 调用锁定方法:lock()
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票,票号为: " + ticket);
ticket --;
}else{
break;
}
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Windows w = new Windows();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
2.2.4 同步监视器(锁)释放的操作
- 当同步代码块和同步方法正常执行结束;
- 当同步代码块和同步方法中遇到了break和return终止截该代码块、该方法的继续执行。
- 当同步代码块和同步方法中出现了未处理的Error和Exception,导致异常结束。
- 当前线程在同步代码块和同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
2.2.5 同步监视器(锁)不会释放的操作
- 线程执行同步代码块或同步方法是,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
- 线程执行同步代码块石,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。
2.3 死锁问题和线程通信
2.3.1 死锁
- 死锁的理解:
不同的线程分别占用对方需要的同步资源不放弃, 都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。 - 说明:
2.1 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
2.2 我们使用同步时,要避免出现死锁。
2.3.2 线程的通信
涉及到的三个方法:
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
三个方法的使用要求:
- wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
- wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
- wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中的。
两个线程交替来 输出1 - 100的数字
class Number implements Runnable{
private int number = 1;
public Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (obj) {
obj.notify();
if(number <= 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得调用如下wait()方法的线程进入阻塞状态
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
sleep()和wait()的异同:
- 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
- 不同点:
2.1 两个方法定义的位置不同:sleep()定义在Thread类中,wait定义在Object类中
2.2 调用要求不同:sleep() 可以在任何场合下调用,wait()只能同步代码块或者同步方法中通过同步监视器来调用。
2.3 是否释放同步监视器:如果两个方法都在同步代码块或者同步方法中使用,sleep()不会释放同步监视器,wait()会直接释放同步监视器。
3. 线程的小知识
1. java中线程的分类:守护线程、用户线程
1.1 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
1.2 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
1.3 Java垃圾回收就是一个典型的守护线程。
2. 生命周期
2.1 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态;
2.2 就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源;
2.3 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能;
2.4 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态;
2.5 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束;