1、程序、进程、线程
1.1 程序
为完成某种特定任务,用某种语言编写的一组指令的集合,它是一段静态的代码
1.2 进程
是程序的一次执行过程,或是正在运行的一个程序,动态的过程
创建-存在-消亡
1.3 线程
是进程的一部分,是程序内部的一条执行路径
每个线程拥有独立的运行栈和程序计数器
进程是程序运行和资源分配的基本单位;
线程是 CPU调度 和 分派 的能够独立运行的基本单位;
并行、并发
- 并行:多个 CPU 同时执行多个任务;比如:多个人同时做不同事
- 并发:一个 CPU “同时”(采用时间片机制)执行多个任务,操作同一个资源;比如:秒杀、多个人做同一件事
2、Thread
创建线程方式 1:
继承 Thread 类,重写 run()方法;
- 不能直接调用
run()
方法启动线程,这样做只是调用了一个类的普通方法;- 不可以再让已经
start()
的线程,再次调用start()
方法,会报异常IllegalThreadStateException
。
2.1 Thread 常用方法
方法名 | 方法作用 |
---|---|
void start() | 此线程开始执行,Java虚拟机调用此线程的run方法 |
void run() | 线程的执行内容 |
String getName() | 返回此线程的名称 |
void setName(String name) | 设置此线程的名称为参数 name |
int getPriority() | 返回此线程的优先级 |
void setPriority(int newPriority) | 更改线程优先等级 |
static Thread currentThread() | 返回对当前正在执行的线程对象的引用。 |
static void sleep(long millis) | 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行) |
void yield() | 当前线程释放 CPU 的执行权(也可能立马又继续 执行了) |
void join() | 在线程 A 中调用线程 B.join(),此时线程 A 进入阻塞 状态,直到线程 B 完全执行完以后,线程 A 才结束阻塞状态 |
void stop()(已弃用) | 停止线 |
2.2 线程的调度
2.2.1 调度策略
时间片:抢占式的,根据线程优先级抢占 CPU 的执行权,高优先级的更容易获得;同优先级线程,先进先出队列。
2.2.2 优先等级
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5(默认)
使用方法:
- void setPriority(int priority):设置线程优先级
- int getPriority():获取线程优先级
2.2.3 注意
- 线程创建时继承父类线程优先级;
- 低优先级只是获取 CPU 调度的概率低,并不一定是高优先级线程之后才被调用的。
2.3 线程的分类
- 守护线程:服务于 用户线程(主线程),如:Java 垃圾回收;
- 若 JVM 中都是守护线程,则当前 JVM 将退出;
- 用户线程(主线程)
- 在
start()
方法前调用thread.setDaemon(true)
可以将一个 用户线程 变成一个 守护线程。
- 在
3、线程的同步机制
1、同步代码块
synchronized(同步监视器){
// 需要被同步的代码:线程对共享数据操作的语句
}
- 操作共享数据的代码,即为需要被同步的代码(不能包含代码多了,也不能包含代码少了);
- 共享数据:多个线程公共操作的变量;
- 同步监视器,俗称
锁
。任何一个类的对象,都可以充当锁;但是多个线程必须共用同一把锁; - Thread 继承类,一般使用
当前类名.class
作为同步监视器; - Runnable接口实现类,可以考虑使用
this
充当同步监视器
2、同步方法
如果操作共享数据的代码完整的声明在一个方法中,不妨使用声明整个方法同步
- 同步方法仍然设计到同步监视器,只是不需要我们显式的声明;
- 非静态的同步方法,同步监视器是:
this
; - 静态的同步方法,同步监视器是:
当前类本身
;
3、Lock锁
java.util.concurrent.locks.lock
实现类:
ReentrantLock、
ReentrantReadWriteLock.ReaLock、
ReentrantReadWriteLock.WriteLock
-
JDK 5.0 开始,java 提供了更强大的线程同步机制,:通过显式定义同步锁对象来实现。同步锁使用
Lock
对象充当; -
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具;锁
提供了对共享资源的独立访问,每次只能有一个线程对Lock
对象加锁,线程开始访问共享资源之前应先获得Lock
对象; -
ReentrantLock
类实现了Lock
,它拥有与Synchronized
相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以显式加锁,释放锁。
实现
- 启动同步:lock();
- 结束同步:unlock()。
ReentrantLock
公平锁:先来后到
非公平锁:可以插队(默认)
4、线程的利弊
- 同步的方式,解决了线程的安全问题。 --> 好处
- 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低下。 --> 局限性
4、线程的通信
wait()
:一旦执行此方法,当前线程进入阻塞状态,并释放同步监视器;
notify()
:一旦执行此方法,就会唤醒被 wait 的一个线程,如果有多个线程被wait,就唤醒优先级高的;
notifyAll()
:一旦执行此方法,就会唤醒所有被 wait 的线程。
说明:
-
wait()
、notify()
、notifyAll()
三个方法必须使用在同步代码块或同步方法中; -
wait()
、notify()
、notifyAll()
三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则,会出现
IllegalMonitorStateException
异常
生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customre)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者视图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
Producer(生产者)
// 生产者:控制产品对象
class Producer extends Thread{
private Product product;
public Producer(){
}
public Producer(Product product){
this.product = product;
}
// 生产产品
public void run(){
System.out.println("生产产品================");
// 调用产品方法生产
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
product.production();
}
}
}
Consumer(消费者)
// 消费者:控制产品对象
class Consumer extends Thread{
private Product product;
public Consumer(){
}
public Consumer(Product product){
this.product = product;
}
// 购买产品
public void run(){
System.out.println("购买产品================");
// 调用产品方法出售
while(true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
product.sell();
}
}
}
Product(产品)
// 产品类
class Product{
private int productCount = 0;
private Object sync = new Object();
// 生产产品
public synchronized void production(){
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName() + "---生产第" + productCount + "件商品");
notify();
} else{
System.out.println(Thread.currentThread().getName() + "---产品数大于 20,wait()执行");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 出售
public synchronized void sell(){
if(productCount > 0){
System.out.println(Thread.currentThread().getName() + "***购买了第" + productCount + "件商品");
productCount--;
notify();
} else{
System.out.println(Thread.currentThread().getName() + "---没有商品了,wait()执行");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行(开始生产、消费):
public class ProductTest {
public static void main(String[] args) {
Product product = new Product();
// 生产线
Producer producer = new Producer(product);
producer.setName("生产者");
// 消费线1
Consumer consumer1 = new Consumer(product);
consumer1.setName("消费者1");
// 消费线2
Consumer consumer2 = new Consumer(product);
consumer2.setName("消费者2");
producer.start();
consumer1.start();
consumer2.start();
}
}
创建线程-3:实现 Callable 接口
JDK 5.0 新增创建线程方式:
- 实现
Callable
接口;- 使用线程池
与使用 Runnable 相比,Callable 功能更强大
- 相比
run()
方法,可以有返回值;- 方法可以抛出异常;
- 支持泛型的返回值;
- 需要借助
FutureTask
类,比如获取返回结果
Future 接口
- 可以对具体
Runnable
、Callable
任务的执行结果进行取消、查询是否完成、获取结果等; FutureTask
是Future
接口的唯一的实现类;FutureTak
同时实现了Runnable
、Future
接口,它既可以作为Runnable
北县城执行,又可以作为Future
得到Callable
的返回值
实现步骤
-
创建一个实现了
Callable
接口的实现类,可实现泛型,即为call()方法的返回值类型; -
实现 call() 方法,将此线程需要执行的操作声明在 call() 中;
// 1、创建一个实现了 Callable 接口的实现类,可实现泛型 class NumThread implements Callable<Integer>{ @Override public Integer call() throws Exception { System.out.println(Thread.currentThread().getName() + "执行-----"); int sum = 0; for(int i = 0; i < 100; i++){ if(i % 2 == 0){ sum += i; } } return sum; } }
-
创建
Callable
接口实现类的对象; -
将
Callable
接口实现类的对象作为参数传递到FutureTask
构造器中,创建FutureTask
的对象; -
将
FutureTask
的对象作为参数传递到Thread
类的构造器中,创建Thread
对象,并调用start()
方法。 -
获取
Callable
接口实现类中call()
方法的返回值。public class Callable_Test { public static void main(String[] args) { // 3、创建`Callable`接口实现类的对象; NumThread numThread = new NumThread(); // 4、将`Callable`接口实现类的对象作为参数传递到`FutureTask`构造器中,创建`FutureTask`的对象; FutureTask<Integer> futureTask = new FutureTask<Integer>(numThread); // 5、将`FutureTask`的对象作为参数传递到`Thread`类的构造器中,创建`Thread`对象,并调用`start()`方法。 Thread thread = new Thread(futureTask); thread.start(); try { // 6、获取`Callable`接口实现类中`call()`方法的返回值。 Integer sum = futureTask.get(); System.out.println(sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
创建线程-4:使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对想能影响很大;
思路:梯田创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建、销毁,实现重复利用。
好处:
提高相应速度(减少了创建新线程的时间);
降低资源消耗(重复利用线程池中线程,不需要每次都创建);
便于线程管理
相关 API
JDK 5.0 起提供了线程池相关的API:
ExecutorService
接口、Executors
工具类
- ExecutorService:真正的线程池接口,常用子类
ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般来执行
Runnable
; - Future submit(Callable task):执行任务,有返回值,一般用来执行
Callable
- void shutdown():关闭连接池
- void execute(Runnable command):执行任务/命令,没有返回值,一般来执行
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程;
- Executors.newFixedThreadPool(int nThreads):创建一个可重用固定线程数的线程池;
- Executors.newSingleThreadExecutor():创建一个使用无界队列运行的单个工作线程的执行任务;
- Executors.newScheduledThreadPool(int corePoolSize):创建一个线程池,可以调度命令在给定的延迟之后运行,或定期运行
实现步骤
public class Executors_Test {
public static void main(String[] args) {
// 1、提供指定线程数的线程池,根据 Executors 工具类的源码可知,该方法返回的对象其实是 ThreadPoolExecutor 类
// 因为 ThreadPoolExecutor --> 继承AbstractExecutorService --> 实现ExecutorService
ThreadPoolExecutor service = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
// 获取到 ThreadPoolExecutor 类可以使用更多的方法来管理线程池。如:
service.setCorePoolSize(20);
// service.setKeepAliveTime(long time,TimeUnit unit);
// service.setMaximumPoolSize(30);
// service.setRejectedExecutionHandler(RejectedExecutionHandler handler);
// service.setThreadFactory(ThreadFactory factory);
// 2、执行指定的线程操作,需要提供实现 Runnable 接口或 Callable 接口的实现类
System.out.println(service.getClass());
// 2.1 使用 Runnable 接口实现类
service.execute(new Number_Runnable());
// 2.2 使用 Callable 接口实现类,并有返回值
Future<Integer> future = service.submit(new Number_Callable());
try {
int sum = future.get();
System.out.println("Callable实现类获取返回值" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
// 3、关闭连接池
service.shutdown();
}
}
// 实现 Runnable
class Number_Runnable implements Runnable{
@Override
public void run() {
int sum = 0;
for (int i = 0; i < 100; i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + "--" + i);
sum += i;
}
}
System.out.println("Runnable--100以内偶数和为:" + sum);
}
}
// 实现 Callable并使用泛型控制返回值类型
class Number_Callable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++){
if(i % 2 == 1){
System.out.println(Thread.currentThread().getName() + "--" + i);
sum += i;
}
}
return sum;
}
}
面试题
1、synchronized 和 Lock的区别
- 相同:二者都可以解决线程安全问题
- 不相同:
synchronized
是内置的java关键字;Lock
是一个java类;synchronized
机制在执行完相应的同步代码块以后,自动的释放锁;Lock
需要手动启动同步(lock()
),同时结束同步也需要手动的实现(unlock()
)。synchronized
可重入锁,不可以中断,非公平的;Lock
可重入锁,可以判断锁,非公平(可以自己设置)- 可重入锁:指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获得过还没释放而阻塞;
- 优先使用顺序:Lock --> 同步代码块(已经进入方法体,分配了相应资源) --> 同步方法(在方法体外)
2、sleep()
与wait()
的区别
sleep()
方法是属于 Thread 类中的;而wait()
方法,则是属于 Object 类的;sleep()
方法可以再任意地方调用;wait()
只能在同步代码块中调用;sleep()
方法使程序暂停执行指定的时间,让出cpu给其他线程,但是它的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。所以在调用sleep()
方法的过程中,线程不会释放对象锁;- 调用
wait()
方法的时候,线程会放弃对象锁,进入等待此线程的等待锁定池,只有针对此对象调用notify()
或notifyAll()
方法后本线程才进入对象锁定池准备获取对象锁进入运行状态;注意:唤醒后,将在wait()
的地方继续执行。