1.使用情况:
Java 在以下场景下使用多线程解决问题:
-
当需要同时执行多个任务时,可以使用多线程提高程序的并发性和执行效率。
-
当需要处理输入/输出操作时,可以使用多线程提高程序的响应速度。
-
当需要进行耗时的操作,如网络请求、数据库操作等时,可以使用多线程来避免阻塞主线程,提高用户体验。
-
当需要进行并行计算时,可以使用多线程将计算任务分配给多个线程并发执行,加快计算速度。
-
当需要实现异步操作时,可以使用多线程来实现任务的异步处理,提高程序的响应速度和并发性。
-
当需要实现服务器端的并发处理时,可以使用多线程来处理多个客户端请求,提高服务器的并发性能。
2.多线程概念
世间万物中每个个体都可以同时完成很多工作。例如,人体可以同时进行呼吸、血液循环、思考题等活动。用户既可以使用计算机听歌,也可以编写文档、发送文件等,这些活动可以同时进行。在领域里,这种同时执行多个操作的行为模式被称为并发。在操作系统中同时运行着多个独立的任务每个任务对应一个进程,每个进程可产生多个线程。通过这种并发运行的方式,计算机的性能被挖掘至了极限.
1.进程与线程
进程是程序的一次动态执行过程,它是从代码加载、执行中到执行完毕的一个完整过程,也是过程本身从产生、发展到最终消亡的过程。操作系统同时管理一个计算机系统中的多个进程,让计算中的多个进程轮流使用中央处理器(Central Processing Unit, CPU)资源,或者共享操作系统的其他资源。由于CPU执行速度非常快,所有程序好像是在"同时"运行一样。
在操作系统中可以有多个进程,这些进程包括系统进程(由操作系统内部建立的进程)和用户进程(由用户程序建立的进程)。可以从Windows任务管理器中查看已启动的进程,进程是系统运行程序的最小单元。各进程之间是独立的,每个进程的内部数据和状态也是完全独立的。
线程是进程中执行运算的最小单位,暗在进程基础上的进一步划分,一个线程可以完成一个独立的顺序控制流程。下面看一简单的问题,假设一个水箱有五个排水孔,打开一个排水孔清空水箱需要一小时,怎样才能使水箱迅速清空并计算最快的清空时间?如果把水箱排水比作一个进程,那么一个排水孔就是一个线程。
与进程不同,同一进程内的多个线程共享同一块内存空间(包括代码空间、数据空间)和一块系统资源,所有系统在产生一个线程或在各线程之间切换工作时,其负担要比在进程间切换小得多。
综上所述,进程和线程是两个不同的概念,应用程序有单进程单线程的,有多进程但每个进程只有一个线程的,有单进程包含多线程的,还有多进程且每个进程有多个线程等四种情况。
如果在同一个进程中同时有多个线程,用于执行不同的工作,则称之为“多线程”。这些线程可以同时存在、同时执行。例如,只有一个排水孔的水箱可比作单线程程序,有多个排水孔的水箱可比作多线程程序。下面详细介绍多线程的运行机制。
2.多线程的运行机制
以往开发的程序大多是单线程的,即一个程序只有从开始到结束这一条执行路径。而多线程是指一个进程同时存在几条执行路径且并发执行的工作方式。
并发运行与并行运行不同。并行运行通常表示同一个时刻有多条指令代码在处理器上同时运行,这种情况往往需要多个处理器支持。而并发运行表示在一个处理器中,操作系统为了提高程序的运行效率,将CPU的执行时间分成多个时间片,分配给同一进程的不同线程。当执行完一个时间片后,当前运行的线程就可能交付出CPU权限,让其他线程执行下一个时间片,当然CPU也有可能相邻的时间片分配给同一线程,即多个线程分享CPU时间,交替运行。之所以从表面上看是多个线程同时运行的,是因为不同线程之间切换的时间非常短,也许仅仅是几毫秒,对普通人来说是难以感知的,即所谓的“宏观并行,微观串行”。
3.多线程的优势
多线程作为一种多作务并发的工作方式,有着广泛的应用。合理使用线程,将减少开发和维护的成本,甚至可以改善复杂应用程序的性能。使用多线程的优势如下。
(1)充分利用CPU的资源。运行单线程程序时,若程序发生阻塞,则CPU可能会处于空闲状态,这将造成计算机资源浪费。而使用多线程可以在某个线程处于休眠或阻塞的状态时运行其他线程,这样将大大提高资源利用率。
(2)简化编程模型:可以考虑将一个既长又复杂的进程分为多个线程,成为几个独立执行的模块,如使用时、分、秒来描述当前时间。如果是单线程程序,则需要多重判断。如果使用多线程,时、分、秒各使用一个线程控制,每个线程仅需实现简单的流程,简化了程序逻辑,更方便编码和维护。
(3)良好的用户体验。由于多个线程可以交替运行,减少或避免了程序阻塞或意外情况造成的响应过慢现象,减少了用户等待时间,提升了用户体验。
(4)多线程日常技术实际开发中是非常有价值的。例如,Word文本编辑工具需提供一边编辑一边保存的功能,且同时进行规范化格式和标记错别字检查;一个浏览器必须能同时下载多个图片;一个Web服务器必须能同时响应多个用户请求;JVM本身就在后台提供了一个超级线程进行垃圾回收……总之,多线程在实际编程中的应用是非常广泛的。
4.线程类的一些常用方法:
(1).start()
Java中的线程通过实现Runnable接口并重写run()方法来定义自己的执行逻辑。当线程对象调用start()方法时,它将启动一个新的线程,该线程会在它的run()方法内部执行自定义的任务逻辑
(2).run()
Java线程的start()方法用于启动一个新线程,新线程会在run()方法中执行自己的任务。需要注意的是,不能直接调用run()方法来启动新线程,否则只会作为普通的方法被调用,并不会创建新的线程。
//补充以下面试题
run()和start()有什么区别??
【答:在Java中,线程的启动有两种方式:调用run()方法和调用start()方法。
调用run()方法并不会创建新的线程。相反,它会在当前执行线程的上下文中直接运行这个方法,也就是说它是普通的方法调用而已,不具备多线程的特性,因此并不能真正实现多线程并发执行。
调用start()方法才能创建一个新的线程,并使其处于就绪状态,等待CPU分配时间片来执行。可以通过继承Thread类或实现Runnable接口的方式来创建一个新的线程对象。然后调用该对象的start()方法来启动这个线程,当获取到CPU时间片后,线程会自动调用run()方法,开始执行线程的任务。总之,只有调用start()方法才能启动一个真正的新线程,使用run()方法则只是单纯地调用了线程的指定代码而已,没有启动新线程】
(3).sleep() 强迫一个线程睡眠N毫秒。
(4).isAlive() 判断一个线程是否存活。
(5).join() 等待线程终止。
(6).activeCount() 程序中活跃的线程数。
(7).enumerate()枚举程序中的线程。
(8). currentThread() 得到当前线程。
(9)isDaemon() 一个线程是否为守护线程。
(10).setDaemon()
设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
(11).setName() 为线程设置一个名称。
(12).wait() 强迫一个线程等待。
(13)notify() 通知一个线程继续运行。
(14).setPriority() 设置一个线程的优先级。
(15).synchronized和Lock比较(重点)
3.实现多线程
在java中要想实现多线程,有三手段,一种是继承Thread(java.lang.Thread类支持),另外一种是实现Runable接口,还有一种是实现Callable接口,并与Future、线程池结合使用
1.Thread
1.Thread类介绍
Thread类提供是了大量的方法来控制和操作过程。、
Thread类的静态方法currentThread()返回当前线程对象的引用。在java程序启动时,一个线程立即随之启动,这个线程通常被称为程序的主线程。publicstatic void main()方法是主线程的入口,每个进程至少有一个主线程。它的重要性如下。
主线程是产生其他子线程的线程。
主线程通常必须最后完成运行,因为它执行各种关闭动作。尽管主线程是自动创建的,但是可以由一个Thread对象控制。因此,可以使用Thread类的方法获取主线程信息。
在java语言中,实现多线程的方式有两种:一种是继承Thread类,另一种是实现Runnable接口下面分别介绍这两种创建线程类的方法。
2.继承Thread类创建线程类
继承Thread类是实现线程的一种方式。在使用此方法自定义线程类时,必须在格式上满足如下要求。
此类必须继承Thread类
将线程执行的代码写在run()方法中
线程从它的run()方法开始执行,即run()方法是线程执行的起点,就像main()方法是应用程序的起点一样。因为run()方法定义在Thread类中,所有的自定义线程类中必须重写run()方法,为线程提供实现具体任务的代码。使用Thread类创建并启动线程的代码结构如下。
//继承Thread类的方式创建自定义线程类
publicclass MyThread extends Thread{
//省略成员变量和成员方法代码
//重写Thread类中的run()方法
public void run(){
//线程执行任务的代码
}
}
public class Thread1 extends Thread{
private String name;
public Thread1(String name) {
this.name=name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name+"运行 " + i);
try {
sleep((int)Math.random()*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread1 thread1 = new Thread1("A");
Thread1 thread2 = new Thread1("B");
thread1.start();
thread2.start();
}
}
2.Runnable
在使用继承Thread类的方式创建线程的过程中,子类无法再继承其他父类。这是因为java语言不支持多重继承。在这种情况下,可以通过实现Runnable接口的方式创建线程。这种方式更具有灵活性,用户线程还可以通过继承,再具有其他类的特性,这是开发中经常使用的方式。
Runnable接口位于java.lang包中,其中只提供一个抽象方法run()的声明,Thread类也实现了Runnable接口。使用Runnable接口时离不开Thread类,这是因为它要用到Thread类中的statr()方法。在Runnable接口中只有run()方法,其他操作都要借助于Thread类。使用Runnable接口创建线程的一般格式如下。
//实现Runnable接口方式创建线程类
classMyThread implements Runnable{
public void run(){
//这里写线程的内容
}
}
//测试类
publicclass RunnableTest{
public static void main(String[] args){
//通过Thread类创建线程对象
MyTherad myThread=new MyThread();
Thread thread =new Thread(myThread);
thread.start();
}
}
在以上代码中,虽然MyThread类为线程提供了run()方法,但它本身不是线程类。如果要创建一个专门执行run()方法的线程对象,则需要创建一个Thread对象。
在上面的代码中,MyThread类实现了Runnable接口,在run()方法中编写线程所执行的代码。如果MyThread还需继承其他类(如Base类),也完全可以实现。关键代码如下。
classMyThread extends Base implements Runnable{
public void run(){
//线程执行任务的代码
}
}
public class Thread2 implements Runnable{
private String name;
public Thread2(String name) {
this.name=name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "运行 " + i);
try {
Thread.sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(new Thread2("C"));
thread1.start();
Thread thread2 = new Thread(new Thread2("D"));
thread2.start();
}
}
3.素数(例子)
public class Thread7 implements Runnable{
private int prime;
private int q;
private int w;
public Thread7(int nn,int mm){
this.q=nn;
this.w=mm;
}
@Override
public void run() {
isPrime(q,w);//判断素数
}
public void isPrime(int min,int max) {
int k=max;
int s=min;
if (max<min){
System.out.println("最大数不能小于最小数,请重新输入");
return;
}else if (max<=0||min<=0){
System.out.println("输入的数据不能为负数");
return;
}
for (int i=k;i>s;i--){
if (i==1){//1不是素数
continue;
}
prime=i%2;//去掉偶数
if (prime==0&&i>3){
continue;
}
prime=i%3;//去掉整除3的数
if (prime==0&&i>7){
continue;
}
prime=i%5;//去掉整除5的数
if (prime==0&&i>7){
continue;
}
prime=i%7;//去掉整除7的数
if (prime==0&&i>7){
continue;
}
System.out.println(Thread.currentThread().getName()+"素数 = " + i);
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(new Thread7(3, 20));
thread1.start();
Thread thread2 = new Thread(new Thread7(3, 15));
thread2.start();
Thread thread3 = new Thread(new Thread7(2, 60));
thread3.start();
}
}
4.线程同步及锁机制
1.为什么需要线程同步
前面学习的线程都是独立且异步运行的,也就是说每个线程都包含了运行时所需要的数据或方法,不必关心其他线程的状态和行为。但是经常会有一些同时运行的线程需要操作共同数据,此时就要考虑其他线程的状态和行为;否则,不能保证程序运行结果的正确性
2.实现线程的同步
当两个或多个线程需要访问同一资源时,需要以某种顺序来确保该资源某一时刻只能被一个线程使用,这称为线程同步。线程同步相当于为线程中需要一次性完成不允许中断的操作加上一把锁,从而解决冲突
当多个线程共享数据时,由于CPU负责线程的调试,所以程序无法精确地控制多线程的交替次序。如果没有特殊控制,则多线程对共享数据的修改和访问将导致数据的不一致。加锁的过程使用线程同步实现,有同步代码块和同步方法两种方式,这两种方式都使用到synchronized关键字。
(1).同步代码块
代码块即使用"{}"括起来的一段代码,使用synchronized关键字修饰的代码块被称为同步代码块。其语法如下。
synchronized(obj){
//需要同步的代码
}
如果一个代码块带有synchronized(obj)标记,那么当线程执行代码时,必须先获得obj变量所引用的对象的锁,其可针对任何代码块,并且可以任意指定上锁的对象,因此灵活性更高。
3.synchronized和Lock的区别
synchronized是Java中的一个关键字,用于实现同步机制,确保线程互斥地访问同步代码块或方法。
synchronized同步方法:
如果一个方法的的呢代码都属于需同步的代码,那么这个方法定义处可以直接使用synchronized关键字修饰,即同步方法。其语法如下。
访问修饰符 synchronized 返回类型 方法名(参数列表) {//省略方法体……}
或
synchronized访问修饰符 返回类型 方法名(参数列表) {//省略方法体……}
而Lock是一个Java接口,在java.util.concurrent.locks包下定义,它提供了比synchronized更广泛的锁操作。
[synchronized]
自动获取和释放锁,无需手动释放
只能用在Java对象或者class上
可重入已被JVM优化,如自适应自旋锁
[Lock]
需要手动获取和释放锁,可以通过try-finally块确保释放
更灵活,可以创建多个条件变量(Condition对象)
可以是公平锁或非公平锁,可以使用Lock接口的其他实现,如ReentrantLock,ReentrantReadWriteLock
Lock锁是Java并发包(java.util.concurrent.locks)提供的一种灵活的显式锁机制,用于控制多个线程对共享资源的访问。与内置锁(通过synchronized关键字隐式实现)不同,Lock接口提供了更多的功能和控制选项。
1.卖票(例子)
public class TicketThread implements Runnable{
private int ticket=100;
Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (obj){
if (ticket>0){
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
public static void main(String[] args) {
TicketThread ticketThread = new TicketThread();
Thread thread1 = new Thread(ticketThread);
Thread thread2 = new Thread(ticketThread);
Thread thread3 = new Thread(ticketThread);
thread1.start();
thread2.start();
thread3.start();
}
}
2.lock
public class LockExample {
private final Lock lock=new ReentrantLock();
private int count;
public void increment(){
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount(){
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockExample lockExample = new LockExample();
//通过runnable多态指向一个工作的多线程的
Runnable task = ()->{
for (int i = 0; i < 1000; i++) {
lockExample.increment();
System.out.println("lockExample.getCount() = " + lockExample.getCount());
}
};
task.run();//启动线程
}
}
5.公平锁,非公平锁,乐观锁,悲观锁,死锁,自旋锁
1.公平锁和非公平锁的区别(多线程执行顺序的维度)
1.概念理解
公平锁:加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程,先来先得。
非公平所:线程加锁时直接尝试获取锁,获取不到就自动到队尾等待。
例子:ReentrantLock 同时支持两种锁
//创建一个非公平锁,默认是非公平锁
Lock nonFairLock= new ReentrantLock();
Lock nonFairLock= new ReentrantLock(false);
//创建一个公平锁,构造传参true
Lock fairLock= new ReentrantLock(true);
2.适用场景
更多的是直接使用非公平锁:非公平锁比公平锁性能高5-10倍,因为公平锁需要在多核情况下维护一个队列,如果当前线程不是队列的第一个无法获取锁,增加了线程切换次数。
公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。
非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。
举个例子:公平锁就像开车经过收费站一样,所有的车都会排队等待通过,先来的车先通过,
Java公平锁和非公平锁的区别_java_脚本之家(公平性和非公平性锁子)
2.乐观锁和悲观锁的区别
1.概念理解
悲观锁:假设一定会发生并发冲突,通过阻塞其他所有线程来保证数据的完整性。
乐观锁:假设不会发生并发冲突,直接不加锁去完成某项更新,如果冲突就返回失败。
乐观锁和悲观锁是两种不同的并发控制策略,它们在处理数据时持有不同的态度和假设。
例子
悲观锁:Synchronized多线程同步,具有排他性,也会容易产生死锁。
乐观锁:CAS机制,简单来说会有三个操作数,当前内存变量值V,变量预期值A,即将更新值B,当需要更新变量的时候,会直接将变量值V和预期值A进行比较,如果相同,则直接更新为B;如果不相同,则当前变量值V刷新到预期值中,然后重新尝试比较更新。
2.适用场景
乐观锁:适用于数据争用不严重/重试代价不大/需要相应速度快的场景。
悲观锁:适用于数据争用严重/重试代价大的场景。
1.悲观锁
态度:悲观地认为如果不严格同步线程调用,那么一定会产生异常。
机制:使用互斥锁将资源锁定,只供一个线程调用,阻塞其他线程。
适用场景:适用于写多读少的情况3。
实现:传统关系型数据库中的行锁、表锁、读锁、写锁等,以及Java中的synchronized和ReentrantLock等独占锁4。
2.乐观锁
态度:乐观地认为别人不会同时修改数据。
机制:不会上锁,但在更新时会判断在此期间别人是否修改了数据,可以使用版本号机制和CAS算法实现
适用场景:适用于写少读多的情况,可以提高吞吐量3。
实现:Java中的java.util.concurrent.atomic包下的原子变量类使用了乐观锁的一种实现方式,即CAS4。
总结:
悲观锁通过锁定资源来避免并发冲突,适用于写多读少的情况,但会导致资源长时间被占用,降低并发性能。乐观锁则通过乐观的态度和检查机制来减少锁的使用,适用于写少读多的情况,可以提高系统的吞吐量,但可能会导致数据不一致的问题。选择哪种策略取决于具体的应用场景和需求。
3.死锁
死锁是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种僵局,无一个进程或线程能够继续执行。在Java中,死锁通常发生在多线程环境中,当两个或多个线程相互等待对方释放资源时,就会发生死锁。
4.自旋锁
自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)
6.线程同步的特征
所谓线程之间保持同步,是指不同的线程在执行以同一个对象作为锁标记的同步代码块或同步方法时,因为要获得这个对象的锁而相互牵制,线程同步具有以下特征。
当多个并发线程访问同一对象的同步代码块或同步方法时,同一时刻只能有一个线程运行,其他线程必须等待当前线程运行完毕后才能运行。
如查多个线程访问的不是同一共享资源,则无需同步。
当一个线程访问Object对象的同步代码块或同步方法时,其他线程仍可以访问该Object 对象的非同步代码块及非同步方法。
综上所述,synchronized关键字就是为当前手代码块声明一把锁,获得这把锁的线程可以执行代码块里的指令,其他的线程只能等待获取锁,然后才能执行相同的操作。
以上学习了使用同步方法和同步代码块实现线程同步,这两者从实现结果上看没有区别,只是同步方法便于阅读理解,而同步代码块可以更精确地限制访问区域,这样会更高效。
7.多线程编程之线程池(Thread Pool)
在Java多线程编程中,线程池(Thread Pool)是一种用于优化线程管理的技术。线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的ThreadFactory创建一个新线程。
创建线程池简单代码
public class ThreadPoolExample1 {
public static void main(String[] args) {
//1.创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(20);
//2.提交任务到线程池指向
for (int i = 0; i < 10; i++) {
executorService.execute(new Work());
}
//3.关闭线程池
executorService.shutdown();
}
}
1.线程池的主要优势在于:
1.降低资源消耗:通过重复利用已创建的线程,避免线程的频繁创建和销毁所带来的性能开销。
2.提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
3.提高系统的稳定性:由于线程数量得到有效控制,可以避免大量的线程导致系统资源耗尽的情况。
4.Java的java.util.concurrent包提供了几个用于创建线程池的工厂方法和类,如Executors.newFixedThreadPool、Executors.newCachedThreadPool、Executors.newSingleThreadExecutor等。这些工厂方法返回实现了ExecutorService接口的线程池对象,该接口定义了一些用于管理线程池的方法,如submit(提交任务)、shutdown(关闭线程池)等。
2.使用线程池时,开发者需要注意以下几点:
1.线程池的大小应根据实际的应用场景和需求来设定,避免过大或过小。
2.对于耗时的任务,应考虑使用异步执行,避免阻塞主线程或线程池中的其他线程。
3.需要合理处理任务执行过程中可能出现的异常,避免影响线程池的稳定性和可用性。
4.当不再需要线程池时,应调用shutdown或shutdownNow方法来关闭线程池,释放资源。
3.java线程池里的代码是按顺序执行的吗
在Java线程池中,代码的执行顺序并不是按照提交任务的顺序来严格保证的。线程池设计的目的就是为了并发执行多个任务,因此它会尽可能地并行处理这些任务,以充分利用多核CPU的性能。
当你向线程池提交任务时(比如通过execute方法),这些任务会被放入一个工作队列中等待执行。线程池中的线程会从这个队列中取出任务并执行。由于线程池中的线程是并发执行的,因此任务的执行顺序是不确定的。
虽然任务的执行顺序不是确定的,但线程池会尽量保证所有任务最终都能被执行,并且每个任务只会被一个线程执行一次。
如果你需要保证任务的执行顺序,那么线程池可能不是最佳选择。在这种情况下,你可能需要使用单线程执行器(Executors.newSingleThreadExecutor()),它会确保任务按照提交的顺序依次执行。但是请注意,这样做会限制并发性,并可能降低性能。
总的来说,线程池中的代码执行顺序不是按照提交的顺序来严格保证的,而是并发执行的。如果你需要顺序执行,那么应该考虑使用其他同步机制或单线程执行器。
public class ThreadPoolExample2 {
public static void main(String[] args) {
//1.创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
//2.提交任务到线程池指向
for (int i = 0; i < 10; i++) {
final int worki=i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程任务"+worki+"正在运行"+Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程任务"+worki+"已经结束啦");
}
});
}
//3.关闭线程池
executorService.shutdown();
}
}