并发:指两个或多个事件在同一时间段内发生(这些事件发生有前后顺序关系)。
并行:指两个或多个事件在同一时刻发生(时间一起发生)。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用来实现进程的调度和管理以及资源分配。
多个线程或进程”同时”运行只是感官上的一种表现。事实上进程和线程是并发运行的,OS的线程调度机制将时间划分为很多时间片段(时间片),尽可能均匀分配给正在运行的程序,获取CPU时间片的线程或进程得以被执行,其他则等待。而CPU则在这些进程或线程上来回切换运行。微观上所有进程和线程是走走停停的,宏观上都在运行,这种都运行的现象叫并发,但是不是绝对意义上的“同时发生。
进程:指一个内存中运行的应用程序,每个进程都有独立的内存空间,一个应用程序可以运行多个进程。
线程:线程是进程中的一个执行单元。
小结:
线程的作用是提供一个轻量级执行单元——虽比进程小,但仍能执行任何java代码。一般情况下,对操作系统来说,一个线程是一个完成的执行单元,但仍属于一个进程,进程的地址空间在组成该进程的所有线程之间共享。也就是说,每个线程都可以独立调度,而且有自己的栈和程序计数器,但会和同个进程中的其他线程共享内存和对象。
线程调度
- 分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间
- 抢占式调度:优先级高的线程使用CPU,如果线程的优先级相同,则随机选择一个(线程随机性),java的使用为抢占式调度
创建线程
创建多线程程序的第一种方式:
创建Thread类的子类
java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类
实现步骤:
1.创建一个Thread类的子类
2.在Thread类中的子类重写run方法
3.创建Thread类的子类对象
4.调用Thread类中的方法start,开启线程,执行run方法
(多次启动一个线程是非法的,特别是当线程已经结束后,不能再重新启动)
(java的线程使用为抢占式调度)
package demo1;
public class MyThread01 extends Thread{
public void run() {
for(int i=0;i<5;i++) {
System.out.println("thread:"+i);
}
}
}
package demo1;
public class myDemo12 {
public static void main(String[] args) throws Exception {
MyThread01 myThread = new MyThread01();
myThread.start(); //开辟新的栈空间,执行run方法
for(int i=5;i<10;i++) {
System.out.println("main:"+i);
}
}
}
运行结果:
main:5
thread:0
main:6
thread:1
main:7
thread:2
main:8
thread:3
main:9
thread:4
或者是:
使用匿名内部类
Thread thread = new Thread() {
public void run() {
for(int i=5;i<10;i++) {
System.out.println("main:"+i);
}
}
};
thread.start();
Thread常用方法
setName(Sting name); //设置线程的名字
getName(); //获取线程的名字
sleep(long ms); //暂定执行线程(线程堵塞,不释放锁)(一般通过Thread类访问)
join(); //让此线程强制执行,正在执行的线程必须等待此线程完成之后才可以继续执行(一般通过Thread实例访问)
interrupt(); //当一个线程运行时,另外一个线程可以直接通过interrupt()方法中断其运行状态
setPriority(Thread.MIN_PRIORITY); //设置线程的优先级为最低(NORM_PRIORITY中等,MAX_PRIORITY最高)
【注意】并非优先级越高就一定会先执行,哪个线程先执行将由 CPU 的调度决定
yield(); //线程礼让,将一个线程的操作暂时让给其他线程执行(线程堵塞,不释放锁)(一般通过Thread类的静态访问)(不一定能暂停执行!!)
wait(); //线程等待,属于Object类,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法 (线程堵塞,释放锁)
notify(); //唤醒wait()线程,属于Object类
这其中比较需要注意2个方法:
一、join(),假设现在有两个线程,A和B,如果在线程A里写上 B.join(),那么线程A会等线程B执行完毕之后才会继续执行线程A。
如果你在主线程调用其他线程时,你希望主线程在结束之前,其他线程能执行完毕,那么就考虑用 join() 吧。
二、yield(),启用该方法的线程会主动让出部分cpu的调度权,但不是全部,具体要看cpu的实际调度
创建多线程程序的第二种方式:
实现Runnable接口
实现步骤:
1.创建一个Runnable接口的实现类
2.在实现类中重写Runnable接口的run方法
3.创建一个Runnable接口的实现类对象
4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
5.调用Thread类对象的start方法
package demo1;
public class ImpRunnable implements Runnable{
@Override
public void run() {
for(int i=0;i<5;i++) {
System.out.println("thread:"+i);
}
}
}
package demo1;
public class myDemo12 {
public static void main(String[] args) throws Exception {
ImpRunnable imp = new ImpRunnable();
Thread run = new Thread(imp);
run.start();
}
}
或者使用匿名内部类
new Thread(new Runnable() {
public void run() {
for(int i=5;i<10;i++) {
System.out.println("main:"+i);
}
}
}).start();
或者用Lambda表达式替代匿名内部类
new Thread(()->{
for(int i=5;i<10;i++){
System.out.println("main:"+i);
}
}).start();
使用Runnable接口的好处:
1.避免了单继承的局限性
2.增强程序的扩展性,降低了程序的耦合性(解耦)
(设置线程任务与开启线程进行隔离)
Callable接口
其实除了Runnable接口,还有一个与之类似的接口可以使用,那就是Callable接口,但是这个接口通常是和线程池工具ExecutorService一起使用的,这里只介绍ExecutorService可以使用submit方法来让一个Callable接口执行。它会返回一个Future,我们后续的程序可以通过这个Future的get方法得到结果。
// 自定义Callable
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模拟计算需要一秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]) throws Exception {
// 使用
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
Future<Integer> result = executor.submit(task);
// 注意调用get方法会阻塞当前线程,直到得到结果。
// 所以实际编码中建议使用可以设置超时时间的重载get方法。
System.out.println(result.get()); //打印:2
}
}
Future
接口
只有几个比较简单的方法:
public abstract interface Future<V> {
public abstract boolean cancel(boolean paramBoolean);
public abstract boolean isCancelled();
public abstract boolean isDone();
public abstract V get() throws InterruptedException, ExecutionException;
public abstract V get(long paramLong, TimeUnit paramTimeUnit)
throws InterruptedException, ExecutionException, TimeoutException;
}
cancel
方法是试图取消一个线程的执行。
注意是试图取消,并不一定能取消成功。因为任务可能已完成、已取消、或者一些其它因素不能取消,存在取消失败的可能。boolean
类型的返回值是“是否取消成功”的意思。参数paramBoolean
表示是否采用中断的方式取消线程执行。
所以有时候,为了让任务有能够取消的功能,就使用Callable
来代替Runnable
。如果为了可取消性而使用 Future
但又不提供可用的结果,则可以声明 Future<?>
形式类型、并返回 null
作为底层任务的结果。
FutureTask类
FutureTask
是实现的RunnableFuture
接口的,而RunnableFuture
接口同时继承了Runnable
接口和Future
接口:
那
FutureTask
类有什么用?为什么要有一个FutureTask
类?前面说到了Future
只是一个接口,而它里面的cancel
,get
,isDone
等方法要自己实现起来都是非常复杂的。所以JDK提供了一个FutureTask
类来供我们使用。
// 自定义Callable,与上面一样
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模拟计算需要一秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]) throws Exception {
// 使用
ExecutorService executor = Executors.newCachedThreadPool();
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
executor.submit(futureTask);
System.out.println(futureTask.get());
}
}
使用上与第一个Demo有一点小的区别。首先,调用submit
方法是没有返回值的。这里实际上是调用的submit(Runnable task)
方法,而上面的Demo,调用的是submit(Callable<T> task)
方法。
然后,这里是使用FutureTask
直接取get
取值,而上面的Demo是通过submit
方法返回的Future
去取值。
在很多高并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能够在高并发环境下确保任务只执行一次。这块有兴趣的同学可以参看FutureTask源码。
以上的内容摘自:RedSpider技术社区的文章——RedSpider社区简介 · 深入浅出Java多线程
提示:
可以通过重载线程类的构造函数来传入参数,以便run方法使用
-
线程中断
目前在Java里还没有安全直接的方法来停止线程,但是Java提供了线程中断机制来处理需要中断线程的情况。
线程中断机制是一种协作机制。需要注意,通过中断操作并不能直接终止一个线程,而是通知需要被中断的线程自行处理。
-
同步与死锁
参考:Java多线程看这一篇就足够了(吐血超详细总结) - Java团长 - 博客园
一个多线程的程序如果是通过 Runnable 接口实现的,则意味着类中的属性被多个线程共享,那么这样就会造成一种问题,如果这多个线程要操作同一个资源时就有可能出现资源同步问题。
解决方法:
同步代码块
synchronized(同步对象){
需要同步的代码
}
class MyThread implements Runnable{
private int ticket = 5 ; // 假设一共有5张票
public void run(){
for(int i=0;i<100;i++){
synchronized(this){ // 要对当前对象进行同步
if(ticket>0){ // 还有票
try{
Thread.sleep(300) ; // 加入延迟
}catch(InterruptedException e){
e.printStackTrace() ;
}
System.out.println("卖票:ticket = " + ticket-- );
}
}
}
}
};
public class SyncDemo02{
public static void main(String args[]){
MyThread mt = new MyThread() ; // 定义线程对象
Thread t1 = new Thread(mt) ; // 定义Thread对象
Thread t2 = new Thread(mt) ; // 定义Thread对象
Thread t3 = new Thread(mt) ; // 定义Thread对象
t1.start() ;
t2.start() ;
t3.start() ;
}
};
运行结果:
卖票:ticket = 5;
卖票:ticket = 4;
卖票:ticket = 3;
卖票:ticket = 2;
卖票:ticket = 1;
原理:当某个线程抢到cpu的执行权时,开始执行run,接着会遇到synchronized代码块,该线程会检查synchronized代码块中是否有锁对象,如果有,则会将锁对象取出来拿到同步中去执行。同步中的线程没有执行完毕是不会释放锁对象的,同步外的线程没有锁,进不去同步。
同步方法
除了可以将需要的代码设置成同步代码块外,也可以使用 synchronized 关键字将一个方法声明为同步方法。
synchronized 方法返回值 方法名称(参数列表){
}
class MyThread implements Runnable{
private int ticket = 5 ; // 假设一共有5张票
public void run(){
for(int i=0;i<100;i++){
this.sale() ; // 调用同步方法
}
}
public synchronized void sale(){ // 声明同步方法
if(ticket>0){ // 还有票
try{
Thread.sleep(300) ; // 加入延迟
}catch(InterruptedException e){
e.printStackTrace() ;
}
System.out.println("卖票:ticket = " + ticket-- );
}
}
};
public class SyncDemo03{
public static void main(String args[]){
MyThread mt = new MyThread() ; // 定义线程对象
Thread t1 = new Thread(mt) ; // 定义Thread对象
Thread t2 = new Thread(mt) ; // 定义Thread对象
Thread t3 = new Thread(mt) ; // 定义Thread对象
t1.start() ;
t2.start() ;
t3.start() ;
}
};
运行结果:
卖票:ticket = 5;
卖票:ticket = 4;
卖票:ticket = 3;
卖票:ticket = 2;
卖票:ticket = 1;
注意:如果synchronized修饰的方法正在处理一个对象,并且把这个对象变成非法状态,那么读取这个对象的另一个方法(没使用synchronized修饰)仍能看到这个不一致的状态
-----------------------《Java技术手册》(第六版)
锁
锁的机制同样能解决线程安全问题
void lock(); //获取锁
void unlock(); //释放锁
使用步骤:
1、在成员位置创建一个ReentrantLock对象
2、在run开始之初获取锁
3、在run结束之前释放锁
package demo1;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class myDemo12 {
public static void main(String[] args){
Lock l = new ReentrantLock();
new Thread(new Runnable() {
public void run() {
l.lock();
for(int i=0;i<10;i++) {
System.out.println("main:"+i);
}
l.unlock();
}
}).start();
}
}
死锁
所谓死锁,就是两个线程都在等待对方先完成,造成程序的停滞,一般程序的死锁都是在程序运行时出现的。
wait() 与 notify()
package demo1;
public class myDemo12 {
public static void main(String[] args){
Object obj = new Object();
new Thread(new Runnable() {
public void run() {
synchronized(obj) {
try{
System.out.println("等待唤醒");
obj.wait();
}catch(Exception e){
e.printStackTrace();
}
System.out.println("唤醒完毕");
}
}
}).start();
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
synchronized(obj) {
obj.notify();
System.out.println("唤醒!");
}
}
}).start();
}
}
---------------------------------2020.10.26-------------------------------------------
今天来回顾线程内容的时候发现了这段代码的运行并没有造成死锁的效果。
预期效果:是第一个线程通过synchronized获得了锁,然后通过wait(),将该线程置于等待状态,等待其他线程唤醒,接着第二个线程通过synchronized获得了锁,唤醒第一个线程,但由于锁还在第一个线程的手里,故不可能拿得到锁,一直在等待获取锁,最后第一个线程手里攥着锁,但是没有等到第二个线程的唤醒,造成死锁效果。
实际效果:第二个线程获得了锁,唤醒了第一个线程,没有造成死锁。。
原因:wait()会把锁释放。。
所以这里要想造成死锁的效果就要把wait()和notify()改掉通过用boolean变量堵塞
package com.example.demo3.service;
import java.util.concurrent.atomic.AtomicBoolean;
public class MyMain {
public static void main(String[] args){
Object obj = new Object();
AtomicBoolean flag = new AtomicBoolean(false);
new Thread(() -> {
synchronized(obj) {
try{
System.out.println("等待唤醒");
while(flag.get()){
Thread.sleep(1000);
System.out.println("被唤醒");
}
}catch(Exception e){
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
synchronized(obj) {
flag.set(true);
System.out.println("唤醒!");
}
}).start();
}
}
还有一个值得注意的点是,notify()不会释放锁,所以要确保负责调用notify()的线程不要堵塞。
以及,wait()和nofity()要在锁里(synchronized)使用。
线程池
一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
4 种线程池
(1)newFixedThreadPool
创建一个固定线程数的线程池,并给线程分配任务
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
(2)newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
(3)newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
(4)newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
参考:https://jingyan.baidu.com/article/c33e3f48fb3015ea15cbb5c3.html
使用步骤:(以newFixedThreadPool为例)
1、使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
2、创建一个类,实现Runnable接口,重写run方法,设置线程任务
3、调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程
4、调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
package demo1;
//2、
public class ImpRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程");
}
}
package demo1;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class myDemo12 {
public static void main(String[] args){
//1、
ExecutorService ex = Executors.newFixedThreadPool(2);
//3、
ex.submit(new ImpRunnable());
ex.submit(new ImpRunnable());
}
}
运行结果:
pool-1-thread-1线程
pool-1-thread-2线程
参考:java技术手册