持续更新中,欢迎收藏,关注,以便查看后续
Java 多线程 -- 从入门到精通
Java线程与线程的区别
- 所有与进程相关的资源,都被记录在PCB(进程控制模块)中。
- 进程是抢占处理机的调度单位。
- 线程属于某个进程,共享进程的资源。
- 线程由堆栈寄存器、程序计数器和TCB(线程控制模块)组成。
- 线程是CPU调度的最小单位,进程是资源分配的最小单位。
- 线程不能看做独立应用,而进程可看做独立应用
- 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
- 线程没有独立的地址空间,多进程的程序比多线程程序健壮
- 进程的切换比线程的切换开销大
多线程的实现方法
- 继承Thread类,重写run方法
- 实现Runnable接口,重写run方法
- 通过Callable和FutureTask创建线程
- 通过线程池创建线程
Thread中start和run方法的区别
- run方法只是thread的一个普通方法调用,还是在主线程里执行,是不会开启多线程的
直接调用Run方法,程序中只有主线程这一个线程,执行路径只有一条,还是要顺序执行,需要run方法体执行完毕,才可执行下面的代码。(相当与普通的方法)
- start方法可启动多线程
start方法启动线程,无需等待run方法体代码执行完毕,可以直接继续执行下面的代码。(真正的实现多线程)
- 代码示例
创建一个MyThread方法继承Thread
public class MyThread extends Thread {
private String name;
public MyThread(String name) {
this.name = name;
}
public void run(){
for (int i = 0 ; i < 100 ; i++){
System.out.println(this.name +"--"+ i);
}
}
}
使用run方法:结果四个方法按照顺序运行,得出结论(直接使用run方法不是多线程)
public class demo {
public static void main(String[] args) {
MyThread my1 = new MyThread("第1个线程");
MyThread my2 = new MyThread("第2个线程");
MyThread my3 = new MyThread("第3个线程");
MyThread my4 = new MyThread("第4个线程");
my1.run();
my2.run();
my3.run();
my4.run();
}
}
使用start方法:结果发现四个方法交替输出,得出结论(start真正的实现多线程)
public class demo {
public static void main(String[] args) {
MyThread my1 = new MyThread("第1个线程");
MyThread my2 = new MyThread("第2个线程");
MyThread my3 = new MyThread("第3个线程");
MyThread my4 = new MyThread("第4个线程");
my1.start();
my2.start();
my3.start();
my4.start();
}
}
Thread和Runnable的关系
- Thread是实现了Runnable接口的类,是Runnable的具体实现,使得run支持多线程;
- 因类的单一继承原则,推荐多使用Runnable接口;
创建一个MyThread方法继承Thread
public class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name) {
this.name = name;
}
public void run(){
for (int i = 0 ; i < 100 ; i++){
System.out.println(this.name +"--"+ i);
}
}
}
Runnable的代码实现
public static void main(String[] args) {
MyRunnable my1 = new MyRunnable("第1个线程");
MyRunnable my2 = new MyRunnable("第2个线程");
MyRunnable my3 = new MyRunnable("第3个线程");
MyRunnable my4 = new MyRunnable("第4个线程");
Thread t1 = new Thread(my1);
Thread t2 = new Thread(my2);
Thread t3 = new Thread(my3);
Thread t4 = new Thread(my4);
t1.start();
t2.start();
t3.start();
t4.start();
}
使用Callable和Future创建线程
获取返回值示例
- 创建实现Callable接口的类
public class MyCallable implements Callable {
public String call() throws Exception {
return "hello world";
}
}
- 获取多线程返回值
@Test
public void testThread1(){
// 1.获取FutureTask对象
MyCallable myCallable = new MyCallable();
FutureTask futureTask = new FutureTask(myCallable);
// 2.开启线程
new Thread(futureTask).start();
try{
String s = (String) futureTask.get();
System.out.println(s);
}catch (InterruptedException e){
e.printStackTrace();
}catch (ExecutionException e){
e.printStackTrace();
}
}
线程返回值的处理方法
- 主线程等待法:循环–检测–睡眠 —》 检测要获取值不为空 停止睡眠
- 使用Thread类的join()方法:阻塞当前线程以等待子线程处理完毕
- 实现Callable接口:通过FutureTask或线程池获取
线程池的创建使用
阻塞队列
容量有限
基于数组的先进先出队列
BlockingQueue< Runnable > workQueue = new ArrayBlockingQueue<>(5);
容量无限
基于链表的先进先出队列
弊端:如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了
BlockingQueue< Runnable > workQueue = new LinkedBlockingQueue<>();
拒绝策略
默认
队列满了之后它将抛出 RejectedExecutionException 异常
RejectedExecutionHandler rejected = new ThreadPoolExecutor.AbortPolicy();
队列满了丢任务不异常,但是线程池将丢弃被拒绝的任务。
RejectedExecutionHandler rejected = new ThreadPoolExecutor.DiscardPolicy();
将最早进入队列的任务删除,然后将被拒绝的任务添加到等待队列中。
RejectedExecutionHandler rejected = new ThreadPoolExecutor.DiscardOldestPolicy();
如果添加到线程池失败,那么主线程会自己去执行该任务
RejectedExecutionHandler rejected = new ThreadPoolExecutor.CallerRunsPolicy();
创建多线程
四种构造方法:
/**
* corePoolSize 核心线程数
* maximumPoolSize 最大线程数
* keepAliveTime idle线程存活时间
* unit 上个参数的单位
* workQueue 线程对象的缓冲队列
* threadFactory 生成线程的工厂
* handler 达到容量后的回调
*/
//1.
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
//2.
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
//3.
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)
//4.
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
线程不安全
示例代码
public class Demo implements Runnable {
private static int sum = 0;
public void run() {
for (int i = 0;i<50000;i++) {
System.out.println(Thread.currentThread().getName() + ":" + (sum++));
}
}
public static void main(String[] args) {
Demo syncThread = new Demo();
Thread thread1 = new Thread(syncThread,"线程1");
Thread thread2 = new Thread(syncThread,"线程2");
Thread thread3 = new Thread(syncThread,"线程3");
Thread thread4 = new Thread(syncThread,"线程4");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
运行截图如下:多次运行结果不一致,这就是线程不安全。正常最后一个值应该为:50000*4-1=199999
解决线程不安全(synchronized)
示例代码
public class Demo implements Runnable {
private static int sum = 0;
public void run() {
synchronized (this) {
for (int i = 0;i<50000;i++) {
System.out.println(Thread.currentThread().getName() + ":" + (sum++));
}
}
}
public static void main(String[] args) {
Demo syncThread = new Demo();
Thread thread1 = new Thread(syncThread,"线程1");
Thread thread2 = new Thread(syncThread,"线程2");
Thread thread3 = new Thread(syncThread,"线程3");
Thread thread4 = new Thread(syncThread,"线程4");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
运行截图如下:最后一个值多次测试都为199999
sleep和wait的区别
- 基本差别
- sleep是thread类的方法,wait是Object类中定义的方法。
- sleep()方法可以在任何地方使用。
- wait()方法只能在synchronized方法或者synchronized块中使用。
- 本质差别
- Thread.sleep只会让出CPU,不会导致锁行为的改变。
- Object.wait不仅仅让出CPU,还会让出已经占有的同步资源锁。
示例代码:
public void run() {
synchronized (this) {
try {
//不仅仅让出CPU,还会让出已经占有的同步资源锁。
this.wait(1000);
for (int i = 0;i<50000;i++) {
System.out.println(Thread.currentThread().getName() + ":" + (sum++));
}
//只会让出CPU,不会导致锁行为的改变。
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
notify与notifyAll的区别
- notify:只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。(随机)
- notifyAll:所有等待该锁的所有线程都会被唤醒,所有被唤醒的线程都将争夺锁,如果某一个线程获得了锁,其他线程将会进入线程等待。
生活小案例:
notify:
好比你在上厕所,外面有很多人在等,但是有一个人是厕所管理员。等你出来的时候,由管理员随机找一个人去上厕所。
notifyAll:
好比你还在上厕所,外面依然有很多人在等待,但是没有厕所管理员。等你出来的时候,大家一起去抢厕所的使用权,当有一个人抢到厕所的时候,其他人重新等待空余的厕所。
线程的六个状态
- 初始(NEW):
- 运行(RUNNABLE):
- 阻塞(BLOCKED):
- 等待(WAITING):
- 超时等待(TIMED_WAITING):
- 终止(TERMINATED):shang
第三步到第五步都属于阻塞状态
查看进程状态代码示例如下
public class Demo{
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyRunnable());
System.out.println(thread.getState());//线程状态:初始(NEW)
thread.start();
System.out.println(thread.getState());//线程状态:运行(RUNNABLE)
//为了保证下方方法体执行完毕,让当前主线程休眠0.1s
Thread.sleep(100);
System.out.println(thread.getState());//线程状态:终止(terminated)
}
}
class MyRunnable implements Runnable{
public void run() {
for (int i = 0; i < 100; i++) {
}
System.out.println("循环完成~");
}
}
补充:
- 进入synchronized时,且没有获取到锁,线程状态 ---- blocked
直到锁被释放。线程状态 ---- runnable- 线程调用wait()或join时,线程状态 ---- waiting
调用notify或notifyAll时,或join的线程执行结束后,线程状态 ---- runnable- 线程调用sleep(time),或wait(time)时,线程状态 ---- timed waiting
当休眠时间结束后,或者调用notify或notifyAll时。线程状态 ---- runnable- 程序执行结束,线程状态 ---- terminated
Thread.yield
使用yield线程会把CPU时间让掉,让所有线程在竞争一次,也就是说也就是谁先抢到谁执行。
代码示例如下:
public class Demo1 extends Thread {
private String name;
public Demo1(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(this.name+"-------"+i);
// 当i为5时,该线程就会把CPU时间让掉,让所有线程在竞争一次,也就是说也就是谁先抢到谁执行。
if (i == 5) {
this.yield();
}
}
}
public static void main(String[] args) {
Demo1 d1 = new Demo1("张三");
Demo1 d2 = new Demo1("李四");
Demo1 d3 = new Demo1("王五");
d1.start();
d2.start();
d3.start();
}
}
什么是线程安全
在多条线程访问的时候,我们在主程序中不需要去做任何的同步,的程序还能按照我们预期的行为去执行,那么我们就可以说这个类是线程安全的。
我们什么时候需要考虑线程安全呢?:多个线程访问同一个资源
· 如果是多个线程访问同一个资源,那么就需要上锁,才能保证数据的安全性。
· 如果每个线程访问的是各自的资源,那么就不需要考虑线程安全的问题,所以这个时候,我们可以放心使用非线程安全的对象
如何实现线程安全
一、synchronized
采用synchronized关键字给代码块或方法加锁
二、Lock
在java 5之后,java.util.concurrent.locks包下提供了另外一种方式来实现线程同步,就是Lock。
三、synchronized和Lock的区别:
- Lock是接口,synchronized是关键字
- Lock可以提高多个线程进行读操作的效率。
- Lock可以让等待锁的线程响应中断,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
- 发生异常时:Lock需要在finally块中释放锁,否则很可能造成死锁现象。synchronized会自动释放线程占有的锁,不会导致死锁现象发生。