以下内容是从任小龙讲师课堂笔记中整理。
线程和进程
进程是指一个内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个应用程序可以同时启动多个进程。比如在windows系统中,一个运行的abc.exe就是一个进程。
线程是指进程中的一个执行任务(控制单元),一个进程可以同时并发运行多个线程。如:多线程下载软件。
一个进程至少有一个线程,为了提高效率,可以在一个进程中开启多个执行任务,即多线程。
多进程:操作系统中同时运行多个程序
多线程:在同一个进程中同时运行多个任务。
进程和线程的区别:
进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
线程:堆空间是独立的,线程消耗的资源比进程小,相互之间可以影响,又称为轻型进程或进程元。
因为一个进程中的多线程是并发运行的,从微观角度上考虑有先后顺序,哪个线程执行完全取决于CPU的调度,程序员控制不了。
我们可以把多线程并发看做是多个线程在瞬间抢CPU资源,谁抢到资源谁就运行,者=这也早就了多线程的随机性。
Java程序的进程至少包含主线程和垃圾回收线程。
线程调度:
计算机通常只有一个CPU时,在任意时刻只能执行一条计算机指令,每一个进程只有获得CPU的使用权才能执行指令。
所谓多进程并发运行,从宏观上看,其实是各个进程轮流获得CPU的使用权,分别执行各自的任务。那么,在可运行池中,会有多个线程处于就绪状态等到CPU,JVM就负责线程的调度,JVM采用的是抢占式调度,没有采用分时调度,因此可以能造成多线程执行结果的随机性。
多线程优势:
1.进程之间不能共享内存,而线程之间共享内存(堆内存)则很简单
2.系统创建进程时需要为该进程重新分配系统资源,创建线程则代价小很多,因此实现多任务并发时,多线程效率更高。
3.java语言本身内置多线程功能的支持,而不是单纯的作为底层系统的调度方式,从而简化;额多线程编程。
多线程是为了同步完成多项任务,不是为了提供程序运行效率,而是通过提高资源使用效率来提高系统的效率。
Java操作进程:
在java中如何去运行一个进程
方法一:使用Runtime类的exec方法
方法二:ProcessBuilder的start方法
示例:创建进程
public class test{ public static void main(String[] args) throws Exception{ Runtime runtime = Runtime.getRuntime(); runtime.exec("notepad"); ProcessBuilder pb = new ProcessBuilder("notepad"); pb.start(); } }
创建和启动线程,传统有两种方式:
方式1:继承Thread类
方式2:实现Runnable接口
线程类(java.lang.Thread):Thread类和Thread子类才能称之为线程类,阅读API。
主线程(main方法运行,表示主线程)
方式1.继承Thread类
步骤:
1.定义一个类A继承于java.lang.Thread类
2.在A类中覆盖Thread类中的run方法
3.我们在run方法中编写需要执行的操作----run方法里的,线程执行体
4.在main方法(线程)中,创建线程对象,并启动线程。
创建线程类: A类 a = new A类();
调用线程对象的start方法:a.start();//启动一个线程
注意:千万不要调用run方法,如果调用run方法好比是对象调用方法,依然还是只有一个线程,并没有开启新的线程。
示例:
class MusicThread extends Thread{ @Override public void run() { for(int i = 1; i < 50; i++){ System.out.println("Music . . ."); try { Thread.sleep(10L); } catch (InterruptedException e) { e.printStackTrace(); } } } } class GameThread extends Thread{ @Override public void run() { for(int i = 1; i < 50; i++){ System.out.println("Game . . ."); try { Thread.sleep(10L); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class test{ public static void main(String[] args){ new MusicThread().start(); new GameThread().start(); } }
方式2:实现Runnable接口
1.定义一个类A实现于java.lang.Runnable接口,A类不是线程类
2.在A类型中覆盖Runnable接口中的方法。
3.在run方法中编写要执行的操作:---run方法里的线程执行体
4.在main方法(线程)中,创建线程对象,并启动线程
创建线程类对象: Thread t = new Thread(new A());
调用线程对象的start()方法 t.start();
示例
class MusicThread implements Runnable{ @Override public void run() { for(int i = 1; i < 50; i++){ System.out.println("Music . . ."); try { Thread.sleep(10L); } catch (InterruptedException e) { e.printStackTrace(); } } } } class GameThread implements Runnable{ @Override public void run() { for(int i = 1; i < 50; i++){ System.out.println("Game . . ."); try { Thread.sleep(10L); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class test{ public static void main(String[] args){ new Thread(new MusicThread()).start(); new Thread(new GameThread()).start(); } }
案例:吃苹果
存在50个苹果,并编号1-50,现在请3个同学(小A,小B,小C)上台表演吃苹果,因为A,B,C三个人可以同时吃苹果,此时得使用多线程技术实现这个案例。
分析:可以定义三个线程对象,并启动线程
每一个同学吃苹果的时候:先展示自己拿到手上的苹果编号,再吃掉苹果(意味着苹果总数减少一个)
方式1:可以使用继承Thread方式实现
class Student extends Thread{ private int num = 50; public Student(String name){ super(name); } @Override public void run() { while(num > 0) { System.out.println(super.getName() + " 吃了编号为 " + (num--) + " 的苹果"); try { Thread.sleep(10L); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class test{ public static void main(String[] args){ new Student("小A").start(); new Student("小B").start(); new Student("小C").start(); } }
Q:运行结果显示,使用继承方式,小A,小B、小C全部吃了50个苹果,为什么?
方式2:可以使用实现Runnable方式实现
class Apple implements Runnable{ private int num = 50; @Override public void run() { while(num > 0) { System.out.println( Thread.currentThread().getName() + " 吃了编号为 " + (num--) + " 的苹果"); try { Thread.sleep(10L); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class test{ public static void main(String[] args){ Apple a = new Apple(); new Thread(a,"小A").start(); new Thread(a,"小B").start(); new Thread(a,"小C").start(); } }
当使用实现方式的时候,A,B,C一共吃了50个苹果,因为三个线程共享同一个Apple对象,而一个Apple对象中有50个苹果。
继承方式和实现方式创建线程的区别:
继承方式:
1.Java中类是单继承的,如果继承了Thread,该类就不能再有其他直接父类了
2.从操作上分析,继承方式更简单,获取线程名字也简单
3.从多线程共享同一个资源上分析,继承方式不能做到
实现方式:
1.Java中类可以实现多接口,此时类还可以继承其他类,并且还可以实现其他接口
2.从操作上分析,实现方式稍微复杂,获取线程名字也比较复杂,得使用Thread.currentThread().getName()获取线程名字
3.从多线程共享同一个资源上分析,实现方式可以做到(是否共享同一个资源,建议使用实现方式)
在这里三个同学完成抢苹果的例子的使用实现方式才合理。
线程不安全的问题:
当线程并发访问同一个资源对象的时候,可能出现线程不安全的问题。
但是,我们分析打印的结果发现没有问题:
意识:看不到问题,可能是我们经验不够,问题出现的不够明显。
为了让问题更明显:
Thread.sleep(10);//当前线程睡10毫秒,当前线程休息,让其他线程抢占资源,经常用来模拟网络延迟
----------------------------
在程序中并不是使用Thread.sleep(10)之后,程序才出现问题,而是使用之后,问题更明显。
为什么23号苹果被吃了两次?
A和C线程最先拿到编号为23的苹果,C线程打印结果,并进行进行num—操作,而A线程还没来得及打印,就进入睡眠。而C线程已经做了-1操作,num已经是22,所以B线程打印22.此时A线程醒来后继续打印23.
问题的原因是: 该操作分成两步,而不是原子操作
1.打印
2.num—
要解决上述多线程并发访问一个资源的安全性问题。
解决方案:保证打印和减1操作同步完成
A线程进入操作的时候,B和C线程只能在外等着,A操作结束,A,B,C才有机会进入代码执行。
-----------------------------
方式1:同步代码块
方式2:同步方法
方式3:锁机制(Lock)
在线程的run方法上不能使用throws来声明抛出异常,只能在方法中使用try-catch处理来处理异常。
原因:子类覆盖父类方法的原则,子类不能抛出新的异常,在Runnable接口中的run方法都没有声明抛出异常。
public abstract void run();
同步代码块:
语法:
synchronized(同步锁)
{
//需要同步操作的代码
}
同步代码块:
语法:
synchronized method()
{
//TODO
}
同步锁:
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制
同步监听对象/同步锁/同步监听器/互斥锁
Java程序运行使用任何对象作为同步监听对象,但一般的,我们把当前并发访问的共同资源作为同步监听对象。
注意:在任何时候,最多允许一个线程拥有同步锁。谁拿到锁就进入代码块,其他线程只能在外等待。
class Apple implements Runnable{ private int num = 50; @Override public void run() { for(int i = 0; i < 50; i++) { synchronized (this) { if (num > 0) { System.out.println(Thread.currentThread().getName() + " 吃了编号为 " + (num) + " 的苹果"); try { Thread.sleep(10L); } catch (InterruptedException e) { e.printStackTrace(); } num--; } } } } } public class test{ public static void main(String[] args){ Apple a = new Apple(); new Thread(a,"小A").start(); new Thread(a,"小B").start(); new Thread(a,"小C").start(); } }
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
synchronized public void doWork(){
//TODO
}
同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(Apple2.class)
不要使用synchronized修饰run方法,否则一个线程就执行完了所有功能,达不到多线程的效果。此时仍然是多线程,但是多线程是串行的。
把需要同步操作的代码块定义在一个新的方法中,且方法使用synchronized修饰,并在run方法中调用。
class Apple implements Runnable{ private int num = 50; @Override public void run() { for(int i = 0; i < 50; i++) { eat(); } } synchronized private void eat(){ if (num > 0) { System.out.println(Thread.currentThread().getName() + " 吃了编号为 " + (num) + " 的苹果"); try { Thread.sleep(10L); } catch (InterruptedException e) { e.printStackTrace(); } num--; } } } public class test{ public static void main(String[] args){ Apple a = new Apple(); new Thread(a,"小A").start(); new Thread(a,"小B").start(); new Thread(a,"小C").start(); } }
synchronized的好与坏:
好处:保证多线在并发访问的同步操作,避免了线程安全性问题。
缺点:使用synchronized的方法或代码块的性能比不用要低一些。
尽量减小synchronized的作用域。
单例
饿汉模式:
public class ArrayUtil { private ArrayUtil(){} private static ArrayUtil instance = new ArrayUtil(); public ArrayUtil getInstance(){ return instance; } }
懒汉模式:
public class ArrayUtil { private ArrayUtil(){} private static ArrayUtil instance = null; public static ArrayUtil getInstance(){ if(null == instance){ instance = new ArrayUtil(); } return instance; } }
懒汉模式存在的问题:当多线程同时访问getInstance且instance等于null时将引入线程不安全问题
改进版:
public class ArrayUtil { private ArrayUtil(){} private static ArrayUtil instance = null; synchronized public static ArrayUtil getInstance(){ if(null == instance){ instance = new ArrayUtil(); } return instance; } }
使用synchronized修饰getInstance方法可解决线程不安全问题,但是引入了性能问题。
双重检查加锁:既实现线程安全,又能够使性能不受很大的影响。
双重检查加锁机制:不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后先检查实例是否存在,如果不存在才进行同步块,此为第一重检查,进入同步块后,再次检查实例是否存在,如果不存在则在同步的情况下创建实例,此为第二重检查。这样一来,就只需要同步一次,从而减少多次在同步情况下进行判断所浪费的时间。
双重检查加锁机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
注意:在java4及以前版本中,很多JVM对于volatile关键字的实现问题会导致双重检查加锁机制失效,因此双重检查加锁机制只能用在JVM5及以上的版本。
提示:由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高,因此一般建议没有特别需要,不要使用。
也就是说,虽然可以使用双重检查加锁机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。
public class ArrayUtil { private ArrayUtil(){} private static volatile ArrayUtil instance = null; public static ArrayUtil getInstance(){ if(null == instance){ synchronized(ArrayUtil.class) { if(null == instance) { instance = new ArrayUtil(); } } } return instance; } }
同步锁
Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Apple implements Runnable{ private final Lock lock = new ReentrantLock(); private int num = 50; @Override public void run() { for(int i = 0; i < 50; i++) { eat(); } } private void eat(){ lock.lock(); if (num > 0) { System.out.println(Thread.currentThread().getName() + " 吃了编号为 " + (num) + " 的苹果"); try { Thread.sleep(10L); num--; } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } } } public class test{ public static void main(String[] args){ Apple a = new Apple(); new Thread(a,"小A").start(); new Thread(a,"小B").start(); new Thread(a,"小C").start(); } }