目录
一、进程(process)
1、概念
进程就是正在运行的一个程序(program),或是程序执行的一次过程。而程序是静态的代码,是为了完成特定任务,用某种编程语言编写的指令集合。进程作为资源的分配单位,系统会在运行时为每个进程分配不同的内存区域。
2、特点
(1)独立性:进程是系统中独立存在的实体,它可以拥有自己的独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
(2)动态性:进程与程序的区别在于,程序只是一个静态的指令集合,进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
(3)并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
3、并行和并发的区别
并发是指在一个时间段内运行多个进程,而且运行在同一个处理机上,所以同一时刻只能够执行一个进程,但由于多个进程被快速的进行切换,给人造成了它们同时执行的感觉。但从微观上来说,它并不是同时进行的;
并行必须在多处理机上进行,是指在同一时刻运行多个进程,每一个处理器运行一个进程。
二、线程(thread)
1、概念
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位(即一个程序内部的一条执行路径)。一个进程可以开启多个线程。
一个程序运行后至少有一个进程,一个进程里包含多个线程。如果一个进程只有一个线程,这种程序被称为单线程,如果一个进程中有多条执行路径被称为多线程程序。
一个进程中可以有多个线程,每个进程有自己独立的内存,多个线程共享一个进程中的内存。
一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一个堆中分配对象,可以访问相同的变量和对象,就使得线程之间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全隐患。
2、进程与线程的区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
3、线程的生命周期
(1) 新建状态(New):当线程对象创建后,即进入了新建状态,如:Thread t = new MyThread();
(2) 就绪状态(Runnable):当调用线程对象的start()方法(t.start()),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
(3) 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
(4) 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态;
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
a) 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
b) 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
c) 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
(5) 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程生命周期图解:
二、多线程创建
有四种:
1、继承Thread类创建线程
//1、创建线程子类 class MyThread extends Thread{ //分线程执行run方法 //2、重写run方法 @Override public void run() { for (int i = 0; i < 1000; i++) { if(i % 2 == 0){ System.out.println(Thread.currentThread().getName()+"-"+i); } } } } public class CreatThread { public static void main(String[] args) { //3、在主线程中创建线程子类对象(主线程创建的) MyThread myThread = new MyThread(); //4、调用start方法,由分线程执行start方法中的内容 //主线程继续向下执行 //给线程设置名称 myThread.setName("分线程"); myThread.start(); //以下代码继续由主线程执行 //给主线程设置名称 Thread.currentThread().setName("主线程"); for (int i = 0; i < 1000; i++) { if(i % 2 == 0){ System.out.println(Thread.currentThread().getName()+"-"+i); } } } }
注意:开启线程只能用start方法,我们直接用thread 对象调用run方法是不会开启线程的,此时的run方法由主线程调用而不是由我们自己创建的线程调用。
常用方法:
2、实现Runnable接口创建线程
首先说一下为什么要使用实现Runnable的方式来创建线程:
由于Java单继承的局限性,当A类已经继承了B类,那么A类就不可以在继承Thread类。此时想要在A类中创建线程,使用继承Thread类的方式就行不通了。所以就有了第二种创建线程的方式-实现Runnable接口。
我们创建MyThread1类来实现Runnable接口,并重写run方法,run方法中是我们想要新创建的线程要执行的内容,然后再实例化MyThread1类。此时我们创建线程对象,并将MyThread1类的实例化对象当作实参传递给Thread的构造方法,这样我们开启线程之后就可以调用MyThread1类的run方法,也就是我们自己定义的线程执行内容。
//1、创建线程子类-实现runnable接口方式 class MyThread1 implements Runnable{ //分线程执行run方法 //2、重写run方法 @Override public void run() { for (int i = 0; i < 1000; i++) { if(i % 2 == 0){ System.out.println(Thread.currentThread().getName()+"-"+i); } } } } public class CreatThread1 { public static void main(String[] args) { //3、在主线程中创建线程子类对象(主线程创建的) MyThread1 myThread1 = new MyThread1(); //我们需要创建线程对象,将我们实现runnable接口的子类对象传过去 Thread thread = new Thread(myThread1); //给线程设置名称 thread.setName("分线程1"); //4、调用start方法,由分线程执行start方法中的内容 thread.start(); //以下代码继续由主线程执行 //给主线程设置名称 Thread.currentThread().setName("主线程"); for (int i = 0; i < 1000; i++) { if(i % 2 == 0){ System.out.println(Thread.currentThread().getName()+"-"+i); } } } }
3、实现Callable接口创建线程
步骤:
(1)声明自定义类,实现Callable接口
(2)重写call方法,有返回值(返回我们需要的值)
(3)创建Callable实现类对象
(4)创建FutureTask类对象,Callable实现类对象为形参,用FutureTask包装实现类对象。
(5)创建Thread类对象,FutureTask类对象作为形参
(6)Thread类对象.start(),开启线程
(7)用FutureTask类对象调用get方法获取call方法的返回值。
具体实现:
public class Test1 { public static void main(String[] args) { // 创建MyCallable对象 Callable<Integer> myCallable = new MyCallable(); //使用FutureTask来包装MyCallable对象 FutureTask<Integer> futureTask = new FutureTask<Integer>(myCallable); //FutureTask对象作为Thread对象的target创建新的线程 Thread thread = new Thread(futureTask); //线程进入到就绪状态 thread.start(); //主线程 for (int i = 0; i < 100; i++) { if (i%2==1) System.out.println(Thread.currentThread().getName() + " " + i); } //获取call()方法返回的结果 int sum = 0; try { sum = futureTask.get(); System.out.println("sum = " + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } class MyCallable implements Callable<Integer> { // 与run()方法不同的是,call()方法具有返回值 @Override public Integer call() { int sum = 0; for (int i = 0; i < 100; i++) { if (i%2==0) System.out.println(Thread.currentThread().getName() + " " + i); sum+=i; } return sum; } }
4、使用线程池创建对象
public class ThreadPool{ private static int POOL_NUM = 10; //线程池数量 public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(5); for(int i = 0; i<POOL_NUM; i++) { RunnableThread thread = new RunnableThread(); //Thread.sleep(1000); executorService.execute(thread); } //关闭线程池 executorService.shutdown(); } } class RunnableThread implements Runnable { @Override public void run() { System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName() + " "); } }
说明:
ExecutorService、Callable都是属于Executor框架。返回结果的线程是在JDK1.5中引入的新特征,还有Future接口也是属于这个框架,有了这种特征得到返回值就很方便了。
通过分析可以知道,它同样也是实现了Callable接口,实现了Call方法,所以有返回值。这也就是正好符合了前面所说的两种分类。
执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。get方法是阻塞的,即:线程无返回结果,get方法会一直等待。
再介绍Executors类:提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
- public static ExecutorService newFixedThreadPool(int n) 创建n个线程的线程池。
- public static ExecutorService newCachedThreadPool() 创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
- public static ExecutorService newSingleThreadExecutor() 创建一个单线程化的Executor。
- public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。
参考:
https://www.cnblogs.com/lwbqqyumidi/p/3804883.html
https://blog.youkuaiyun.com/u011480603/article/details/75332435/
5、守护线程(daemon)
线程分为用户线程和守护线程,虚拟机必须确保用户线程执行完毕之后才能将线程关闭,但是虚拟机不用等待守护线程执行完毕就可以将守护线程关闭。
在用户线程执行完毕之后,虚拟机也会将守护线程关闭,虚拟机关闭需要一点时间,在关闭的这一个时间段内,守护线程依然会继续执行。
设置守护线程:thread.setDaemom(true)//:默认是false,表示用户线程。
例如:
public class DaemonThread {
public static void main(String[] args) throws InterruptedException {
MyDaemon myDaemon = new MyDaemon();
Thread thread = new Thread(myDaemon);
//设置守护线程
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("用户main线程"+i);
}
}
}
class MyDaemon implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("守护线程"+i);
}
}
}
用户main线程会执行完,但守护线程不会执行完。
三、线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
1、线程同步
由于多个线程执行的不确定性引起执行结果的不稳定,多个线程对账本的共享,会造成操作的不完整性,破坏数据。比如你的银行账户有100元,你和你女朋友同时去银行取钱,她取80,你取50,若是在同一时间取的话且不考虑线程同步问题,那么你的账户最后可能会为负数,这明显是不合理的,所以提出线程同步问题。
Java中提供了线程同步机制,它能够解决上述的线程安全问题。
同步:实现排队效果,不抢占资源,但可以保证数据安全。
异步:不排队,抢占资源,效率高但不保证数据安全。
线程同步的方式有两种:
(1)同步代码块: 在代码块声明上 加上synchronized
synchronized (锁对象) {
可能会产生线程安全问题的代码
}
同步代码块中的锁对象是需要增删改的任意对象。
(2)同步方法:在方法声明上加上synchronized
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步方法中的锁对象是当前类本身。
注:静态同步方法: 在方法声明上加上static synchronized
public static synchronized void method(){
可能会产生线程安全问题的代码
}
静态同步方法中的锁对象是 类名.class。
解释:synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法对象锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
缺陷:若将一个大的方法声明为synchronized将会影响效率。
(3)JUC安全类型的集合:CopyOnWriteArrayList
注意:同步的方式虽然解决了线程同步问题,但是在操作同步代码时只允许一个线程参与,其他线程只能等待,相当于是一个单线程的处理过程,降低了效率。
2、死锁
不同的线程分别占用对方所需要的资源,并且不放弃,都在等对方放弃自己所需要的资源,就形成了线程的死锁。出现死锁之后,程序不会出现异常,也不会出现提示,只是所有的线程都处于阻塞状态,无法继续下一步操作。
3、Lock锁
解决线程安全的问题
1、Lock是一个接口,它的一个实现类ReentrantLock中有两个方法:
(1)lock():给以下的代码上锁,保证运行期间只有一个线程执行;相当于同步锁中的同步监视器。
(2)unlock():解锁
2、synchronized与Lock的异同?
相同点:都是为解决线程安全问题。
不同点:同步方法或同步代码块中是必须执将当前的代码块(作用域)执行完毕才可以将同步监视器释放掉。而 Lock是手动解锁,即可以在任意时刻选择inlock,更加灵活。而且使用Lock,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)。
3、优先使用顺序:Lock->同步代码块->同步方法
注意:必须要保证对于将要执行的若干线程所使用的lock是唯一的。在Runable实现类中lock是唯一的,但是在Thread继承类中,声明多个线程,那么在使用lock的时候会导致lock的不唯一,必须将lock静态化。
例如:
public class LockThread {
public static void main(String[] args) {
SaleTicket saleTicket = new SaleTicket();
new Thread(saleTicket).start();
new Thread(saleTicket).start();
new Thread(saleTicket).start();
}
}
class SaleTicket implements Runnable{
private final ReentrantLock lock = new ReentrantLock();
int ticket = 10;
@Override
public void run() {
while (true){
try{
lock.lock();
if(ticket > 0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticket--);
}else {
break;
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}