并发与并行
- 并发:多个事件在同一个时间段内发生
- 并行:多个事件在同一时刻发生
在操作系统中,并发指的是宏观下一段时间内多个程序运行,在单核心cpu系统中,每一时刻只能有一道程序执行,即在微观下这些程序是分时交替运行的,只是给人的感觉是同时运行而已,这是因为分时交替运行的时间非常短
而在多核心cpu系统中,这些可以并发执行的程序便可以分配到多个cpu上并行执行,即每个处理器都处理一个可以并发执行的程序,这样多个程序便可以同时执行。
线程与进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程,进程也是程序的一次执行过程,是系统运行程序的基本单位,系统运行一个程序也即是一个进程从创建,运行到消亡的过程
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。多线程程序中含有多个线程。
线程调度
- 分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用cpu的时间
- 抢占式调度:优先让优先级高的线程使用CPU,如果线程优先级相同,那么会随机选择一个。Java使用的便是抢占式调度。
创建线程类
Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流,即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。
Java中通过继承Thread创建并启动多线程的步骤如下:
1.定义Thread子类,并重写run()方法,该方法的方法体就代表了线程需要执行的任务,因此将run()方法称之为线程执行体
2.创建Thread子类实例,即线程对象
3.调用线程对象的start()方法来启动该线程
代码
测试类:
public class Demo01{
public static void main(String[] args){
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//开启多线程
mt.start();
//在主线程中执行for循环
for (int i = 0;i < 10;i++){
System.out.println("主线程!" + i)
}
}
}
自定义线程类
public class MyThread extends Thread{
//定义指定线程名称的构造方法
public MyThread(String name){
//调用父类带参构造,指定线程名称
super(name);
}
//重写run方法完成线程执行的任务
@override
public void run(){
for(int i = 0;i < 10;i++){
System.out.println(getName() + ":正在执行" + i)
}
}
}
在程序启动运行主方法的时候,java虚拟机启动了一个进程,主线程main在main()调用的时候被创建,随着调用mt对象的start()方法,另一个新的线程也就启动了,这样,整个应用就在多线程下运行。
多线程执行时,在栈内存中,每一个执行线程都有一片自己所属的栈内存空间,进行方法的压栈和弹栈。
当线程的任务结束了,线程自动在栈内存中释放了,但是当所有的执行线程都结束了,那么进程也就结束了
Thread类
构造方法:
public Thread()
:分配一个新的线程对象public Thread(String name)
:分配一个指定名字的新线程对象public Thread(Runnable target)
:分配一个带有制定目标的新线程对象public Thread(Runnable target,String name)
:分配一个带有制定目标的新线程对象并指定名字
常用方法:
public String getName()
:获取当前线程名称public void start()
:使此线程开始执行,java虚拟机会调用此线程run()方法public void run()
:定义此线程要执行的任务public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)public static Thread currentThread()
:返回对当前正在执行的线程对象的引用
创建线程方式二
即采用java.lang.Runnable,也需要重写run()方法
步骤:
1.定义Runnable接口的实现类,并且重写该接口的run()方法,此方法方法体也是该线程所需要执行的任务
2.创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象
3.调用线程对象的start()方法来启动线程
代码
public class MyRunnable implements Runnable{
@override
public void run(){
for (int i = 0;i < 20;i++){
System.out.println(Thread.currentThread().getName() + " " + i)
}
}
}
public class Demo{
public static void main(String[] args){
//创建自定义类对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr,"小强");
t.start();
for (int i = 0;i < 20;i++){
System.out.println("旺财" + " " + i)
}
}
}
通过实现Runnable接口,使得该类有了多线程类的特征,run()方法是多线程程序的一个执行目标。所有多线程代码都在run方法内,Thread类实际上也是实现了Runnable接口的类
在启动多线程时,需要先通过Thread构造方法Thread(Runnable target)构造处对象,然后调用Thread对象的start()方法来运行多线程代码
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的,因此不管继承Thread类还是实现Runnable接口实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础
tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类包含的Run()方法仅作为线程执行体,而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法
Thread和Runnable的区别
如果一个类继承了Thread,则不适合资源共享,但是如果实现了Runnable接口的话则很容易实现资源共享
总结
实现Runnable接口比继承Thread类更具优势:
1.适合多个相同的程序代码的线程去共享同一个资源
2.可以避免java中单继承的局限性
3.增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立
4.线程池只能放入实现Runnable或Callable类线程,不能直接放入继承Thread类
在java中,每次程序运行至少启动两个线程,一个为主线程,一个为垃圾收集线程,每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实就是在操作系统中启动了一个进程
实现Runnable与Callable的区别
实现Runnable的线程重写的Run方法是无返回值类型的,而Callable重写的Run方法有返回值类型
匿名内部类方式实现线程创建
使用线程的匿名内部类方式可以更方便地实现每个线程执行不同的线程任务操作
使用匿名内部类的方式实现Runnable接口,重写其run方法
public class NoNameInnerClassThread{
public static void main(String[] args){
Runnable r = new Runnable(){
public void run(){
for (int i = 0; i < 20 ; i ++){
System.out.println("张宇" + i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 20; i++){
System.out.println("费玉清" + i);
}
}
}
线程安全
如果有多个线程同时运行,而这些线程可能会同时运行这段代码,程序每次运行的结果和单线程运行结果是一样的,而且其他的变量的值也和预期一样,那就是线程安全的
线程安全问题都是由全局变量以及静态变量引起的,若每个线程中对全局变量和静态变量只有读操作,没有写操作,一般来说这个全局变量就是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响到线程安全。
线程同步
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题
要解决上述多线程并发访问一个资源的安全性问题,Java中提供了同步机制(synchronized)来解决
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制
有三种方式完成同步操作:
1.同步代码块
2.同步方法
3.锁机制
同步代码块
同步代码块:关键字synchronized可以用于方法中的某个区块,表示只对这个区块的资源实行互斥访问
格式
synchronize(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁
1.锁对象:可以为任意类型
2.多个线程对象要使用同一把锁
注意:在任何时候,做多允许一个线程拥有同步锁,谁拿到锁就能进入代码块,其他的线程只能在外等着(BLOCKED)
使用同步代码块解决代码
public class Ticket implements Runnable{
private int ticket = 100;
Object lock = new Object();
//执行卖票操作
@override
public void run(){
//每个窗口卖票的操作
//窗口永远开启
while(true){
synchronized(lock){
if(ticket > 0){//有票可以卖
//出票操作
//使用sleep模拟出票时间
try{
Thread.sleep(50);
}catch(InterruptedException e){
//TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程名
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖" + ticket--);
}
}
}
}
}
当使用了同步代码块之后,上述的安全问题就解决了
同步方法
- 同步方法:使用synchronized修饰的方法就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着
格式
public synchronized void method(){
可能会产生线程安全的代码
}
同步方法的同步锁:
对于非static方法,同步锁就是this
对于static方法,同步锁就是当前方法所在类的字节码对象(类名.class)
使用同步方法代码如下:
public class Ticket implements Runnable{
private int ticket = 100;
Object lock = new Object();
//执行卖票操作
@override
public void run(){
//每个窗口卖票的操作
//窗口永远开启
while(true){
sellTicket();
}
}
//锁对象即是调用这个方法的对象,也就是this
public synchronized void sellTicket(){
if(ticket > 0){//有票可以卖
//出票操作
//使用sleep模拟出票时间
try{
Thread.sleep(50);
}catch(InterruptedException e){
//TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程名
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖" + ticket--);
}
}
}
Lock锁
java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块和方法所具有的功能Lock都具有,除此之外更体现面向对象
Lock锁也称为同步锁,加锁和释放锁方法化了
public void lock()
:加同步锁public void unlock()
:释放同步锁
使用:
public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
@override
public void run(){
while(true){
lock.lock();
if(ticket > 0){//有票可以卖
//出票操作
//使用sleep模拟出票时间
try{
Thread.sleep(50);
}catch(InterruptedException e){
//TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程名
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖" + ticket--);
}
lock.unlock();
}
}
}
线程状态概述
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中含有六种线程状态:
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动,还没有调用start方法 |
Runnable(可运行) | 线程在java虚拟机中运行的状态,可能正在运行自己的代码,也可能没有,取决于操作系统处理器 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁已经被其他线程持有时,该线程会进入Blocked状态,当其持有锁时,会进入Runnable状态 |
Waiting(无限等待) | 一个线程在等到另一个线程执行唤醒动作时,该线程会进入Waiting状态,进入这个状态后线程不能自动唤醒,必须等待其他线程调用notify或notifyAll方法才能唤醒 |
Timed Waiting(计时等待) | 同waiting,有几个方法有超时参数,调用他们将进入Timed Waiting状态,这一状态将一直保持到超时期满或者接收到唤醒通知(Thread.sleep(),Object.wait()) |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获异常终止了run方法而死亡 |
sleep方法和wait方法区别
wait方法是Thread静态方法,调用之后当前线程会释放对象锁
sleep是Object对象锁的方法,调用之后当前线程不会释放对象所
ThreadLocal解决线程安全问题原理
ThreadLocal会复制一份源共享数据进行操作,复数线程之间使用的数据相对独立互不影响。