JAVA基础复习(七)

    本着重新学习(看到什么复习什么)的原则,这一篇讲的是JAVA的多线程。看了诸位大神的解释后详细的查了一些东西,记录下来,也感谢各位在网络上的分享!!!

    mark一下:https://www.cnblogs.com/television/p/9462214.html(非常清晰,每个问题都很精髓,得弄懂)

    多线程这一块一直是我比较薄弱的,也是强行拿出来写一些东西,为了能够巩固和扎实所学,也能更多的去用实践验证理论。在日常的JAVA使用中,一般不会考虑到线程的问题,我总会在跟时间和资源相关的调优时想到多线程。今天也从时间和资源的方向思考几个场景并以此入手,前提就是有一个四人团队,团队接了很多的项目

    (1).在团队中所有的事情都是一个人在做,那么会出现什么情况?怎么处理?

    会出现一人忙碌,三人等待。解决办法就是四个人一起做。

    (2).如果项目非常复杂,需要消耗很长的时间,那么会出现什么情况?怎么处理?

    会出现单位时间无法完成项目,客户需要无休止的等待。解决办法就是四个人一起想办法处理。

    可以看到,第一个例子就是从资源的角度考虑,我们的程序一直在单线程的执行,对于大部分的简单应用来说,单线程就能实现。但是对于那些复杂的逻辑来说,单线程去跑程序肯定是不科学的。因为我们还拥有着很多其他线程可供使用。第二个例子就是从时间的角度考虑,单线程会导致程序的使用者会为了程序中的某些操作(如大量的I/O操作,文件操作等)而长时间的等待,这也是不合理的。所以最好的解决办法就是另开线程。这是为了能够更快的完成计算任务,更好的完成程序服务,同时也能更有效的利用CPU资源。知道了多线程目的其实也就等于是知道了使用多线程的优势。那么接下来先看一下线程和进程的概念。

    1.什么是进程?

    在计算机中,我们可以同时开启多个任务,每一个任务之间都是交替执行的,从而达到了一种同时进行的状态。而每一个任务,我们就可以称之为一个进程(但不是每一个任务都只有一个进程),进程是应用程序运行的载体。

    2.什么是线程?

    在某些进程中还需要同时执行多个子任务,如我们在使用WORD时,我们通过打字键入内容,我们还可以看到WORD在对我们键入的内容进行拼写检查,那么这些就是子任务,也就是线程。进程和线程之间的关系比较密切,一个进程可以包含一个或多个线程,线程是操作系统调度的最小任务单位,并且如何调度线程是完全由操作系统决定的,也就是说程序自己不能决定执行时间或执行时长。线程是由进程创建出来的,但是线程没有独立的地址空间(内存空间),进程拥有。

    实现多任务的方法有多进程模式(每个进程只有一个线程),多线程模式(一个进程有多个线程),多进程+多线程模式。第三种方式是复杂度最高的,暂且不论,单纯考虑多线程和多进程的话,进程间相互独立所以使用多进程的稳定性较于使用多线程会高,在进程之间由于数据是分开的,所以共享比较复杂,需要使用IPC进行进程间通信,并且多进程只需通过增加该程序在CPU中的进程数就能提高性能,但是进程在创建的开销和通信速度上不占优势。相反,线程由于创建在同一个进程之中,所以通信速度快,但是由于数据是在一起的,所以需要考虑数据共享的问题,并且一个线程崩溃将有可能导致整个进程崩溃。所以多进程和多线程在不同的系统中,面对不同的应用程序的需求,也是仁者见仁智者见智的。当然,能互通有无,彼此互补当然是更好的。但是一般情况下,在面对大量计算或者WEB服务时,会优先使用多线程,而集群等顾名思义需要分布式的时候优先使用多进程。在JAVA中,一个JAVA程序实际上就是一个JVM进程,JVM会使用一个主线程来执行main方法,但在main方法中又可以开启多个线程。

    3.如何创建线程?

    创建线程的方式包括继承Tread类,重写run方法;实现Runnable接口;实现Callable接口。针对前两种方式,主线程创建了Tread对象,但是真正执行Tread对象中重写的run方法的是在主线程使用start方法后JAVA虚拟机开启的新的子线程。而且只有在Thread对象中,即使用了start方法,才是开启一个新线程,直接使用run方法相当于是直接调用一个类内定义方法,并不创建新线程。这三种方式的区别最简单的就在于继承Thread类后便无法再继承其他类了,但是编写简单,获取线程号也只需要使用this.getName(),而后两者的劣势在于访问线程必须使用Thread.currentThread()方法,并且较为复杂。但是优势就是避免了单继承,多个线程间可以共享一个代理对象(target对象),对多线程访问数据的场景非常合适。而后两者之间的区别就在于Callable接口有返回值Future,而Runnable接口没有返回值。如下所示,我们还可以通过线程池来管理已经创建的线程对象。

package com.day_7.excercise_1;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class TryThread{
	
	public static class ExtendsThread extends Thread{
		@Override
		public void run() {
			System.out.println("Extends Thread Type : new Thread");
		}
	}
	public static class ImplementRunnable implements Runnable{
		@Override
		public void run() {
			System.out.println("Implement Runnable Type : new Thread");
		}
	}
    public static class ImplementCallable implements Callable<String> {
        @Override
        public String call() {
        	return "Implement Callable Type : new Thread";
        }
    }
	public static void main(String[] args) throws Exception {
		// 1.继承Thread类
		ExtendsThread extendsThread = new ExtendsThread();
		extendsThread.start();
		System.out.println(extendsThread.getName());
		// 2.实现Runnable接口
		Thread implementRunnable = new Thread(new ImplementRunnable());
//		ImplementRunnable implementRunnable = new ImplementRunnable();
		implementRunnable.start();
		System.out.println(Thread.currentThread());
		// 3.实现Callable接口
        FutureTask<String> futureTask = new FutureTask<String>(new ImplementCallable());
        new Thread(futureTask).start();
		System.out.println(Thread.currentThread());
		
        // 创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(3);
        Future<String> future = pool.submit(new ImplementCallable());
        pool.submit(new ImplementRunnable());
        pool.submit(new ExtendsThread());
        System.out.println(future.get());
		
        // 中断线程
        extendsThread.interrupt();
        implementRunnable.interrupt();
        futureTask.cancel(true);
        pool.shutdown();
		
	}
}

    线程的生命周期

    (1).初始状态:当程序使用new关键字创建一个线程后,该线程还不能使用,只是被JVM分配了内存进行初始化。

    (2).就绪状态:当线程对象使用了start方法后该线程就处于就绪状态。就绪状态在等待CPU进行调度,也就是等待CPU分配资源,谁先获得CPU资源,谁先开始执行。

    (3).运行状态:进入运行状态代表着就绪状态的该线程获得了CPU资源,并开始执行run方法内的方法体,run方法内定义了线程的具体操作和功能实现。若再给定时间内没有完成run方法,将会重新回到就绪状态。

    (4).阻塞状态:处于运行状态的线程可能因为sleep方法或wait方法等进入阻塞状态。在阻塞状态的线程将暂时停止运行,并归还CPU资源。并只有在解决阻塞的具体原因时重新进入就绪状态,等待CPU重新分配资源。

    (5).终止状态:线程正常执行run方法结束,出现异常终止或使用stop方法强制结束(不推荐),会进入终止状态。

    JAVA线程的状态包括:New(创建),Runnable(运行中),Blocked(阻塞),Waiting(等待),Timed Waiting(计时等待),Terminated(终止)。

    与此同时,在JAVA中存在CPU资源分配的监控器,即线程调度器,用于监控处于就绪状态 的所有线程。并可以通过判断线程的优先级来决定哪些线程有优先执行的权利。JAVA中优先级可以用(1~10)表示,低优先级(1~4),默认优先级(5),高优先级(6~10)。可以使用Thread.setPriority(int n)来进行优先级的设定,优先级高的线程被调用的优先级最高,但是不能通过设置优先级的高低来保证功能的执行顺序。 

    4.什么是线程安全与线程不安全?

    在使用多线程的情况下,由于不同的线程要访问同一份数据,所以会出现对于数据访问的安全问题,即能否在使用多线程时也能保持数据的顺序调用申请值的一致。线程安全也就是数据结果一致,并且某一个线程对该数据进行修改后,另一个线程得到的是修改值而不是原值。线程不安全则相反。线程安全需要确保数据的一致性,所以需要对数据进行控制,增加了开销,但也保证了线程间的数据读写顺序。线程安全的类包括不变类(String,Integer,LocalDate,因为其一旦创建,实例内部的成员变量不能改变,所以多线程不能写只能读,不需要同步),没有成员变量的类(Math,因为在类中只提供了静态方法,自身没有成员)和正确使用synchronized类(StringBuffer)。非线程安全的类不能在多线程中共享实例并修改(ArrayList),但是可以在多线程中只读方式共享。那如何保证线程安全呢?

读取方法通常也需要同步

对共享变量进行写入时,必须保证是原子操作(即不能被中断的一个或一系列操作),JVM规范了几种原子操作(基本类型(long和double除外)的赋值),引用类型的赋值。

    (1).使用局部变量,局部变量不需要同步。

    (2).Synchronized关键字:同步的本质就是给指定的对象加锁,加锁对象必须是同一个实例

    

 加锁对象数量多线程访问同一个对象同步代码块
对象锁(普通方法)调用方法的这一个对象不受影响,每一个对象拥有自己的锁
类锁(类)该类的所有对象受影响,所有对象共享同一个锁
静态方法锁(同类锁)该类的所有对象受影响,所有对象共享同一个锁
同步代码块调用代码块的这一个对象不受影响,每一个对象拥有自己的锁

    当synchronized关键字修饰一个时,称之为类锁或全局锁。其修饰关键字是static sychronized,含义是无论创建多少个该类的对象,都共享同一个锁,所以每一个对象都会按照要求顺序进行操作。

package com.day_7.excercise_1;

public class TrySynchronized5 extends Thread{
    public static int salary = 0;
    public void run(){
    	// 1.
        addSalary();
        // 2.
//        minusSalary();
    }
    public static synchronized void addSalary() {
    	System.out.println("================");
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName()+"线程调用该方法开始时salary值为:"+salary);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        salary += 1;
        System.out.println(Thread.currentThread().getName()+"线程调用该方法结束后salary值为:"+salary);
    }
    public synchronized void minusSalary() {
    	System.out.println("================");
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName()+"线程调用该方法开始时salary值为:"+salary);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        salary -= 1;
        System.out.println(Thread.currentThread().getName()+"线程调用该方法结束后salary值为:"+salary);        
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
        	TrySynchronized5 trySynchronized5 = new TrySynchronized5();
            new Thread(trySynchronized5).start();
        }
    }
}

    如上,我在类中定义了一个使用static synchronized修饰的加法操作,一个仅用synchronized修饰的减法操作(即场景1和场景2)。而后在主函数中我循环创建对象,并开启线程。在方法中我需要该方法在等待500ms后输出一句语句,并且有对应的开头和结束。那么在使用减法操作时,结果如左图,并且没有任何等待时间,这说明线程之间争抢了资源,导致了数据的混乱。而在使用加法操作时,结果如右图,是顺序出现的,并且有对应的等待时间,说明线程之间没有进行争抢,即每一个线程执行时其他线程都是在等待资源锁释放的。这就是类锁,使得该类的所有对象使用同一个锁进行资源分配和等待。

    当synchronized关键字修饰实例方法(不包括静态方法)时,称之为实例锁或对象锁。其作用是给当前实例加锁,可以将整个方法变成同步代码块。当一个线程使用同步代码前需要首先获取当前实例的锁。与类锁不同的是,对象锁每个对象都拥有自己的锁,多线程访问同一个对象的同步代码块时不受影响。

package com.day_7.excercise_1;

public class TrySynchronized6 extends Thread{
    int salary = 0;
    public void run(){
    	// 1.
        addSalary();
        // 2.
//        minusSalary();
    }
    public synchronized void addSalary() {
    	System.out.println("================");
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName()+"线程调用该方法开始时salary值为:"+salary);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        salary += 1;
        System.out.println(Thread.currentThread().getName()+"线程调用该方法结束后salary值为:"+salary);
    }    
    public void minusSalary() {
    	System.out.println("================");
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName()+"线程调用该方法开始时salary值为:"+salary);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        salary -= 1;
        System.out.println(Thread.currentThread().getName()+"线程调用该方法结束后salary值为:"+salary);
    }
    public static void main(String[] args) {
    	TrySynchronized6 trySynchronized6 = new TrySynchronized6();
        for (int i = 0; i < 10; i++) {
            new Thread(trySynchronized6).start();
        }
    	// 3.
//        TrySynchronized6 trySynchronized62 = new TrySynchronized6();
//        for (int i = 0; i < 10; i++) {
//            new Thread(trySynchronized62).start();
//        }
    }
}

    如上,还是相同的例子,但是这次在main函数中是创建一个对象,循环开启线程。相同的结果,减法操作时,结果如左图,加法操作如右图,并且如果我将场景3的注释打开,也会出现,相当于是同时创建了两个对象,两个对象都要进行相同的等待和操作,结果是两个对象调用方法都会有条不紊的顺序执行加法操作,减法操作则会完全混乱,我就不放图片了。

    当synchronized关键字修饰静态方法时,使用的应该是synchronized关键字和static修饰符,这就相当于是使用了一个类锁,故作用范围等与类锁相同。

    当synchronized关键字修饰代码块时,解决的是过多的同步方法会影响JAVA程序运行效率,所以尽量在使用同步时更加精准范围,故使用synchronized修饰代码块来缩小覆盖代码面积。

    (3).使用ThreadLocal类,ThreadLocal使得每一个线程拥有自己的一套变量(每一个Thread线程内部都有一个Map,Map中存储的是本地对象作为key,变量副本作为value),也就是说ThreadLocal会为每一个线程提供相互独立的变量副本,所以每一个线程间在任意时刻都互不影响。但是ThreadLocal也有着自己的问题,如无法处理需要更新共享变量的场景,还有若在使用ThreadLocal后不使用remove方法,会导致内存泄露

package com.day_7.excercise_1;

public class TrySynchronized7 { 
    private static ThreadLocal<Integer> numList = new ThreadLocal<Integer>() {  
        public Integer initialValue() {  
            return 0;  
        }  
    };  
    public int getNext() {  
        numList.set(numList.get() + 1);  
        return numList.get();  
    }  
    public static class Client extends Thread {  
        private TrySynchronized7 trySynchronized7;  
  
        public Client(TrySynchronized7 trySynchronized7) {  
            this.trySynchronized7 = trySynchronized7;  
        }  
  
        public void run() {  
            for (int i = 0; i < 3; i++) {  
            	try {
					Thread.sleep(500);
					System.out.println("thread[" + Thread.currentThread().getName() + "] --> num["  
	                         + trySynchronized7.getNext() + "]");  
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
            }  
        }  
    }
  
    public static void main(String[] args) {  
    	TrySynchronized7 trySynchronized7 = new TrySynchronized7();  
    	Client client_1 = new Client(trySynchronized7);  
    	Client client_2 = new Client(trySynchronized7);  
    	Client client_3 = new Client(trySynchronized7);  
    	client_1.start();  
    	client_2.start();  
    	client_3.start();  
    	numList.remove();
    }  
  
}

    (4).使用Lock接口:在使用synchronized关键字时,由于无法直观判断该线程是否获取了锁的状态,并且在多线程操作时,若某个线程在使用资源时阻塞,则其他线程只能一直等待,这就会形成死锁的局势,加之synchronized线程在执行完同步代码块或者在出现异常后会自动释放锁。故可以使用Lock,Lock中存在tryLock()方法,通过判断返回值(true/false)就可以判断该线程是否获取到了锁,并且可以或者说必须手动释放锁(一般在try/catch/finally的finally中释放,加锁lock(),释放锁unlock())。并且Lock锁不会一直等待锁资源,在Lock中有多种获取锁的方式。

    在Lock中存在的只有6个方法,即lock(用来获取锁,若锁已经被其他线程获取,则等待),lockInterruptibly(用来获取锁,若锁被其他线程获取,则当前线程被禁用线程调度,并处于interrupt中断状态),tryLock(用来尝试获取锁,若获取成功返回true,获取失败返回false,在无法获取锁资源时不会一直等待而是会立即返回),tryLock(long time,TimeUnit unit)(用来尝试获取锁,若获取成功或者在time时间内获取成功,则返回true,在获取失败情况下等待time时间,若在time时间内还未获取到锁,则返回false),unlock(用来释放锁资源),newCondition(用来返回一个多线程间协调通信的工具类,用于监管线程)。常用的是实现了Lock接口的ReentrantLock类,ReentrantLock是一个可以重入锁,这一点与synchronized关键字使用的锁相同。还是之前的例子,改成使用ReentrantLock,结果与使用synchronized关键字是相同的,但是可见的是显式的开锁关锁,更加清晰。

package com.day_7.excercise_1;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TryLock2 extends Thread{
    int salary = 0;
    private Lock lock = new ReentrantLock();
    public void run(){
        addSalary();
    }
    public void addSalary() {
    	lock.lock();
    	System.out.println("================");
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName()+"线程对象调用该方法开始时salary值为:"+salary);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
			lock.unlock();
		}
        salary += 1;
        System.out.println(Thread.currentThread().getName()+"线程调用该方法结束后salary值为:"+salary);
    }    
    public static void main(String[] args) {
    	TrySynchronized6 trySynchronized6 = new TrySynchronized6();
        for (int i = 0; i < 10; i++) {
            new Thread(trySynchronized6).start();
        }
    }
}

    (5).volatile关键字:在使用volatile关键字时,是保证了线程在每次使用变量时,都会读取到变量修改后的最终值,即一个线程修改了变量的值后,其他线程都是立即使用新值的(基于CPU指令,内存屏障)。volatile只能保证单次读/写的原子性,而对于i++(读取变量i的值——>变量i数值加1——>结果回写)这种操作,不能保证其原子性。只有在变量状态独立时才使用volatile关键字。可以配合CAS(java.util.concurrent包建立在CAS之上,没有CAS就没有并发包)使用,但是CAS开销很大,仍然只能保证一个共享变量原子性,并存在ABA问题。(没看懂,mark一下。。。)

    5.什么是守护线程?

    守护线程是为其他线程服务的线程,所有非守护线程都执行完毕后,虚拟机退出,否则即使主线程已经执行结束,程序也不会结束。守护线程不能持有资源,但是我们一般把守护线程用作日志或者监控线程。要注意守护进程的设定时间一定要在该线程开启之前,也就是先thread.setDaemon(true)而后thread.start(),因为已经开启的线程无法设定为守护线程。如下所示,在不使用场景1,即不设定守护线程时,主线程方法执行结束,KidThread线程依然在执行。而在设定了守护线程后,会在主线程结束后便结束。

package com.day_7.excercise_1;

public class TryDaemon {
	public static void mainMethod() {
		for (int i = 0; i < 10; i++) {
			try {
				Thread.sleep(500);
			} catch (Exception e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
	public static class KidThread extends Thread {
		public void run() {
			while (true) {
				try {
					Thread.sleep(1000);

				} catch (Exception e) {
					// TODO: handle exception
				}
				System.out.println(Thread.currentThread().getName());
			}
		}
		public static void main(String[] args) {
			Thread thread = new KidThread();
			// 1.
//			thread.setDaemon(true);
			thread.start();
			mainMethod();
			System.out.println("主线程执行完毕...");

		}
	}
}

    说实话写到这里我已经有点蒙了,毕竟多线程的东西还太多太多,诸如各种锁,volatile和CAS的底层问题等很多的内容都还没有涉及到,近期去看个视频课程理解一下,找时间再开写这块知识。同样,感谢网上的各种资源,让我有了各种瞎尝试的可能性和明确错误原因的可能性。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无语梦醒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值