并发编程1 - 线程基础及其常见方法

一. 进程与线程

1. 概念

进程:
进程可以视为程序的一个实例。当一个程序被运行,从磁盘加载这个程序的代码至内存,就开启了一个进程。
进程是资源分配的最小单位

线程:
线程是调度的最小单位,一个进程内可以有多个线程。线程由一系列指令组成。

二者对比:
进程间相互独立,线程存在于进程内,是进程的一个子集;
进程拥有共享的资源,如内存空间,供内部的线程共享;
进程间通信较为复杂,不同计算机之间的进程通信需要通过网络和协议;
线程通信较为简单,因为它们共享进程间的内存,如多个线程可以访问进程中同一个共享变量;
线程更家轻量,线程的上下文切换成本一般比进程上下文切换更低。

2. 并发与并行

并发(concurrent):
操作系统使用任务调度器将cpu的时间片分给不同的线程使用。线程轮流使用CPU的做法称为并发。
微观串行,宏观并行。
在这里插入图片描述
并行(parallel):
多核cpu下,多个线程同时运行。
在这里插入图片描述
并发是同一时间应对多件事情的能力;
并行是同一时间动手做多件事情的能力。

3. 线程的应用

(1)异步调用

同步:方法调用时需要等待结果返回,才能继续运行;
异步:方法调用时不需要等待结果返回,就能继续运行。

多线程的应用:让方法执行由同步变成异步。如长时间的文件操作,可以通过新开一个线程进行处理,避免阻塞主线程。

(2)提升效率

充分利用多核cpu的优势,提高运行效率。

注意:只有多核cpu才能提高效率,单核时即使是多个线程仍然是轮流执行。单核cpu下,多线程只是用于并发进行多个任务。

二. Java 线程

1. 创建和运行线程

方法一:

//new 一个线程对象
Thread thread = new Thread() {
   @Override
   public void run() {
        int i = 10;
        while (i > 0) {
            System.out.println(this.getName() + ": I am " + i--);
        }
    }
};
thread.setName("线程1");
thread.start(); //执行线程

方法二(推荐):
Thread + Runnable,把线程和任务代码分开

Runnable runnable = new Runnable() {
	@Override
	public void run() {
		System.out.println("runnable……");
	}
};
Thread thread = new Thread(runnable);
thread.start();

Runnable 还可以使用 lambda 表达式进行简化(只有一个方法的类可以使用lambda进行简化):

Runnable runnable = () -> System.out.println("runnable……");
Thread thread = new Thread(runnable, "线程2");
thread.start();

方法三:FutureTask

FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
	@Override
	public Integer call() throws Exception {
		System.out.println("running");
		Thread.sleep(5000);
		return 100;
	}
});
futureTask.run();
System.out.println(futureTask.get()); //get方法会进行阻塞,等futureTask线程走完以后再获取值

2. 查看进程线程

Windows

tasklist #查看所有进程
tasklist | findstr java #带关键字查找
jps #查看所有Java进程
taskkill /F /PID 进程号 #杀死进程 		

Linux

ps -ef  #查看所有进程
ps -ef | grep java
kill 进程号 #杀死进程
top #查看动态进程情况
top -H -p 进程号 #查看该进程中的所有线程信息

3. 线程上下文切换

含义:因为某些原因导致 cpu 不再执行当前线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当进行上下文切换时,程序计数器会记录下一条jvm指令的执行地址,频繁的上下文切换会影响性能,线程数尽量小于 cpu 核数

4. 常见方法

(1) start 与 run 方法
thread.start() 与 thread.run() 的区别:前者是启动了一个线程,由thread线程执行run方法,后者直接调用 run 其实是主线程调用了 run 方法,不能起到提高效率或者并发执行的效果。

(2)sleep 与 yield 方法

  • sleep 方法会让当前线程从 running 进入到 timed waiting(阻塞)状态::
Thread.sleep(1000) //写在哪个线程里就表示休眠哪个线程
  • 其他线程可以使用 interrupt 方法唤醒正在睡眠的线程,此时睡眠的线程会抛出 InterruptedException 异常。
  • 睡眠结束后的线程未必会立刻得到执行(时间片未到)。
  • 建议用 TimeUnit 的sleep代替 Thread 的 sleep,可读性更高。

yield 将当前线程从 running 进入 runnable 就绪状态,然后调度执行其他线程。

区别:任务调度器会将时间片分给就绪状态的线程,但不会分给阻塞状态的线程。

sleep 应用:在 while(true) 中添加 sleep,避免空转浪费 cpu。

(3)线程优先级

thread.setPriority()

线程优先级会提示调度器优先调度该线程,但调度器可以忽略它。
如果cpu比较忙,优先级高的线程会获得更多的时间片,如果cpu比较闲,那优先级几乎没作用。

(4)join 方法

join() 方法用于等待某个线程运行结束后再继续执行。

public class ThreadTest {
    static int a = 0;
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1);
                    a = 2;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        thread.setName("线程1");
        thread.start();
        thread.join(); // 不加 join a =  0; 加了 join a = 2
        System.out.println("a = " + a);
    }

join 应用:
1)线程同步:当等待多个线程执行结束时,等待时间取决于用时最长的那个;
2)限时同步:join方法可以添加等待时间。

(5)interrupt 方法

a. 唤醒处于阻塞状态的线程:会抛出InterruptedException异常,打断标记为 false;
b. 打断正常运行的线程:打断标记为true,可以用于使得线程优雅地退出
两种状态interrupt打断原理

Thread thread = new Thread() {
	@Override
	public void run() {
		while (true) {
			System.out.println("执行中……");
			//获取打断标记,可以优雅地退出线程
			boolean interrupted = Thread.currentThread().isInterrupted();
			if (interrupted) {
				System.out.println("被打断了,退出循环");
				break;
			}
		}
	}
};
thread.setName("线程1");
thread.start();
Thread.sleep(2000);
thread.interrupt(); //不能真正让线程退出,只是使得打断标记为 true,真正线程退出还得靠线程自己
System.out.println("打断标记 " + thread.isInterrupted()); // true

两阶段终止模式:

问题:如何在一个线程 T1 中如何优雅地(给T2料理后事的机会)终止线程 T2?
错误方法:1)调用线程的stop方法,stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就没有机会释放锁,其他线程将永远无法获取锁;2)使用 System.exit(int) 方法停止线程,这种方法会将整个进程都停止。

设计一个程序实现下列功能:
在这里插入图片描述

public static void main(String[] args) throws InterruptedException {
    Thread monitor = new Thread(() -> {
        while (true) {
            Thread current = Thread.currentThread();
            boolean interrupted = current.isInterrupted();
            if (interrupted) {
                System.out.println("料理后事,退出循环");
                break;
            }
            try {
                Thread.sleep(1000); // 此时被打断会抛异常,打断标记为false
                System.out.println("执行监控记录"); //此时被打断,打断标记为true
            } catch (InterruptedException e) {
                System.out.println("抛异常");
                e.printStackTrace();
                current.interrupt(); //需要重新设置打断标记,此时为true
            }
        }
    });

    monitor.start();
    Thread.sleep(3500);
    monitor.interrupt();
}

在这里插入图片描述

小知识
isInterrupted 与 interrupted(static方法) 的区别:前者判断完不会清除打断标记,后者判断完会将打断标记重新置为false。

interrupt 还可以打断 park 的线程,使之继续运行

public static void main(String[] args) throws InterruptedException {
 	Thread monitor = new Thread(() -> {
        System.out.println("park……");
        LockSupport.park();
        System.out.println("unpack……");
    });
    monitor.start();
    Thread.sleep(2000);
    monitor.interrupt(); //打断park线程使之继续运行
}

(6)不推荐使用的方法

stop(停止)、suspend(暂停)、resume(恢复) 方法

(7)守护线程

默认情况下,Java进程需要等待所有线程结束都运行结束才会结束。守护线程是只要等其他非守护线程都运行结束了,即使守护线程还在运行也会被强制结束。

public static void main(String[] args) throws InterruptedException {
	Thread monitor = new Thread(() -> {
        while(true) {
            
        }
    });
    monitor.setDaemon(true); // 将monitor线程设置为守护线程
    monitor.start();
    Thread.sleep(2000);
    System.out.println("结束"); //主线程结束后monitor线程也会停止
}

垃圾回收线程是典型的守护线程,当主线程结束时垃圾回收线程会被强制结束。

5. 线程的状态

(1). 操作系统层面

初始状态:在语言方面创建了线程对象,还未与操作系统线程关联;
可运行状态(就绪状态):线程已经被创建(与操作系统线程关联),可以由CPU调度执行;
运行状态:得到cpu运行时间片;
阻塞状态:如果调用了阻塞API,如读写文件,这时线程会让出CPU,导致线程上下文切换,进入阻塞状态;等IO操作完毕,由操作系统唤醒阻塞的线程,转至可运行状态;
终止状态:线程执行完毕,生命周期结束。

在这里插入图片描述
(2). JAVA层面

Thread.sleep(1000) // timed_waiting
join() // waiting
等待锁释放 // blocked
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《并发编程实战》是一本经典的并发编程书籍,其中包含了丰富的代码示例。这本书的代码示例非常有实战意义,可以帮助开发者在处理并发编程中的各种问题时提供参考。其中的代码示例主要涉及线程池、CAS、原子操作、锁、并发容器、BlockingQueue、CyclicBarrier和Semaphore等相关知识点。 本书的代码示例分布在各个章节中,开发者可以根据需要选择不同的示例进行学习和实践。例如,在线程池相关章节,作者提供了诸如ThreadPoolExecutor、ExecutorCompletionService等类的实现,并且提供了基于可扩展的ThreadPoolExecutor来实现动态调节线程池大小的代码示例。这些示例可以帮助开发者深入了解线程池的实现方式,以及如何进行线程池的调优。 在锁相关章节,作者提供了诸如ReentrantLock和读写锁ReentrantReadWriteLock等类的实现,并且提供了一些实际应用场景下的代码示例,例如票务系统和登录系统。这些示例可以帮助开发者了解锁的原理及其使用方法。 本书同时也介绍了一些常用的并发容器,例如ConcurrentHashMap、ConcurrentLinkedQueue等,在使用这些容器时需要注意线程安全的问题。作者为这些容器提供了详细的使用方法和代码示例,帮助开发者了解如何高效地使用这些容器。总之,《并发编程实战》的代码示例非常有价值,具有一定参考和借鉴意义,可以帮助开发者更好地掌握并发编程知识。 ### 回答2: 《Java并发编程实战》一书的源码是该书的大部分内容的实现代码。这些代码的使用可以帮助读者更好地理解并发编程的实现方式,同时也可以成为读者自己学习并发编程的参考资料。 该书的源码包括一些经典的并发编程实现,例如线程池、锁、原子变量、阻塞队列等。这些实现具有实用性和普遍性,可以帮助读者在自己的开发中解决并发编程问题。同时,该书的源码还包括一些基于实际场景的例子,让读者可以更好地理解并发编程在实际项目开发中的应用。 在使用该书源码时,读者需要关注一些细节问题,例如多线程环境下的原子性、可见性和有序性等。同时,读者还需要学会如何调试和排查多线程程序的问题,以保证程序的正确性和稳定性。 总之,该书的源码是学习并发编程的重要工具之一,读者需要认真学习源码并结合实际项目开发进行练习。只有这样,才能真正掌握并发编程的技巧和应用。 ### 回答3: 《Java并发编程实战》是一本著名的并发编程领域的经典著作,其中的源代码涵盖了Java并发编程的多个方面,非常有学习和参考的价值。 该书中的源代码主要包括了多线并发线程池、ThreadLocal、锁、信号量、条件等一系列并发编程相关的实例和案例,涵盖了从最基础并发操作到应用场景的实践。 通过学习并实践这些源代码,我们可以更好地理解并发编程的思路和原理,掌握并发编程的技能和方法,提高代码质量和性能。同时,还可以培养我们的编码思维和能力,为我们今后的编程工作和研究打下坚实的基础。 总之,《Java并发编程实战》的源代码是具有非常实用和价值的,并发编程相关领域学习者和从业者都可以将其作为一个良好的学习和实践资源,不断探索和尝试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值