程序,进程,线程的基本概念+并行与并发:
程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的过程。-------生命周期
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径 即:线程《线程(一个程序可以有多个线程)
程序:静态的代码 进程:动态执行的程序
线程:进程中要同时干几件事时,每一件事的执行路径成为线程。
并行:多个CPU同时执行多个任务,比如:多个人同时做不同的事
并发:一个CPU(采用时间片)同时执行多个任务,比如秒杀平台,多个人做同件事
线程的调度 调度策略: 时间片:线程的调度采用时间片轮转的方式 抢占式:高优先级的线程抢占CPU Java的调度方法: 1.对于同优先级的线程组成先进先出队列(先到先服务),使用时间片策略 2.对高优先级,使用优先调度的抢占式策略
多线程的创建方式有两种
1.实现runbale接口重写run方法
2.继承Thread类
start与run方法的区别:
start方法的作用:
1.启动当前线程
2.调用当前线程的重写的run方法(在主线程中生成子线程,有两条线程)
调用start方法以后,一条路径代表一个线程,同时执行两线程时,因为时间片的轮换,所以执行过程随机分配,且一个线程对象只能调用一次start方法。 run方法的作用:在主线程中调用以后,直接在主线程一条线程中执行了该线程中run的方法。(调用线程中的run方法,只调用run方法,并不新开线程)
总结:我们不能通过run方法来新开一个线程,只能调用线程中重写的run方法(可以在线程中不断的调用run方法,但是不能开启子线程,即不能同时干几件事),start是开启线程,再调用方法(即默认开启一次线程,调用一次run方法,可以同时执行几件事)
Runnable接口应该由其实例旨在由线程执行的任何类实现。 该类必须定义一个名为run的无参数方法。 此接口旨在为希望在活动时执行代码的对象提供通用协议。 例如, Runnable是由类Thread实现的。 处于活动状态仅意味着线程已启动且尚未停止。 此外, Runnable提供了使类处于活动状态而不是子类化Thread 。 通过实例化Thread实例并将自身作为目标传入,实现Runnable的类可以在不继承Thread的情况下运行。 在大多数情况下,如果您只打算覆盖run()方法而不打算覆盖其他Thread方法,则应该使用Runnable接口。 这很重要,因为除非程序员打算修改或增强类的基本行为,否则类不应被子类化
多线程例子
1.实现Runnable接口重写run方法
package com.qf;
public class runableTest {
public static void main(String[] args) {
Window3 window3 = new Window3();
Thread t1 = new Thread(window3);
Thread t2 = new Thread(window3);
Thread t3 = new Thread(window3);
t1.setName("售票员1");
t2.setName("售票员2");
t3.setName("售票员3");
t1.start();
t2.start();
t3.start();
}
}
class Window3 implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"当前售出第"+ticket+"张票");
ticket--;
}else {
break;
}
}
}
}
2.继承Thread类
package com.qf;
import java.awt.*;
public class threadTest extends Thread {
public static void main(String[] args) {
Window window1 = new Window();
Window window2 = new Window();
Window window3 = new Window();
window1.setName("售票口1");
window2.setName("售票口2");
window3.setName("售票口3");
window1.start();
window2.start();
window3.start();
}
static class Window extends Thread{
private int ticket=100;
@Override
public void run() {
while (true){
if (ticket>0) {
try {
if (ticket>0){
sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"当前售出第"+ticket+"张票");
ticket--;
} else {
break;
}
}
}
}
}
比较创建线程的两种方式: 开发中,优先选择实现Runable接口的方式 原因 1:实现的方式没有类的单继承性的局限性 2:实现的方式更适合用来处理多个线程有共享数据的情况 联系:Thread也是实现自Runable,两种方式都需要重写run()方法,将线程要执行的逻辑声明在run中
3.新增的两种创建多线程方式 1.实现callable接口方式: 与使用runnable方式相比,callable功能更强大些: runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值 方法可以抛出异常 支持泛型的返回值 需要借助FutureTask类,比如获取返回结果
package com.qf;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) {
Window window = new Window();
FutureTask futureTask=new FutureTask(window);
Thread t1 = new Thread(futureTask);
t1.setName("售票员1");
t1.start();
try {
Object o = futureTask.get();
System.out.println(o);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
static class Window implements Callable{
private int ticket=100;
@Override
public Object call() throws Exception {
while (true){
if (ticket>0){
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"当前售出第"+ticket+"张票");
ticket--;
}else {
break;
}
}
return ticket;
}
}
}
4.使用线程池的方式:
背景:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池之,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。类似生活中的公共交通工具。(数据库连接池) 好处:提高响应速度(减少了创建新线程的时间) 降低资源消耗(重复利用线程池中线程,不需要每次都创建) 便于线程管理
package com.qf;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.*;
public class ThreadPoolTest {
public static void main(String[] args) {
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("售票员-%d").build();
ExecutorService executorService = new ThreadPoolExecutor(5,200,0L, TimeUnit.MILLISECONDS,new LinkedBlockingDeque<Runnable>(1024),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());
Window1 window1 = new Window1();
Window2 window2 = new Window2();
executorService.execute(window1);
executorService.execute(window2);
executorService.shutdown();
}
}
class Window1 implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"当前售出第"+ticket+"张票");
ticket--;
}else {
break;
}
}
}
}
class Window2 implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"当前售出第"+ticket+"张票");
ticket--;
}else {
break;
}
}
}
}
这里贴一张阿里Java开发规范 需要的java依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
线程通信方法:
wait()/ notify()/ notifayAll():此三个方法定义在Object类中的,因为这三个方法需要用到锁,而锁是任意对象都能充当的,所以这三个方法定义在Object类中。 由于wait,notify,以及notifyAll都涉及到与锁相关的操作
wait(在进入锁住的区域以后阻塞等待,释放锁让别的线程先进来操作)---- Obj.wait 进入Obj这个锁住的区域的线程把锁交出来原地等待通知
notify(由于有很多锁住的区域,所以需要将区域用锁来标识,也涉及到锁) ----- Obj.notify 新线程进入Obj这个区域进行操作并唤醒wait的线程 有点类似于我要上厕所,我先进了厕所关了门,但是发现厕所有牌子写着不能用,于是我把厕所锁给了别人,别人进来上厕所还是修厕所不得而知,直到有人通知我厕所好了我再接着用。 所以wait,notify需要使用在有锁的地方,也就是需要用synchronize关键字来标识的区域,即使用在同步代码块或者同步方法中,且为了保证wait和notify的区域是同一个锁住的区域,需要用锁来标识,也就是锁要相同的对象来充当
一个线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
下图显示了一个线程完整的生命周期。
-
新建状态: 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。 -
就绪状态: 当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。 -
运行状态: 如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。 -
阻塞状态: 如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种: -
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。 -
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。 -
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会
进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
- 死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
线程的同步:在同步代码块中,只能存在一个线程。
线程的安全问题:
什么是线程安全问题呢?
线程安全问题是指,多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题。
上述例子中:创建三个窗口卖票,总票数为100张票
1.卖票过程中,出现了重票(票被反复的卖出,ticket未被减少时就打印出了)错票。
2.问题出现的原因:当某个线程操作车票的过程中,尚未完成操作时,其他线程参与进来,也来操作车票。(将此过程的代码看作一个区域,当有线程进去时,装锁,不让别的线程进去) 生动理解的例子:有一个厕所,有人进去了,但是没有上锁,于是别人不知道你进去了,别人也进去了对厕所也使用造成错误。
3.如何解决:当一个线程在操作ticket时,其他线程不能参与进来,直到此线程的生命周期结束 4.在java中,我们通过同步机制,来解决线程的安全问题。 方式一:同步代码块 使用同步监视器(锁) Synchronized(同步监视器){ //需要被同步的代码 } 说明: 1. 操作共享数据的代码(所有线程共享的数据的操作的代码)(视作卫生间区域(所有人共享的厕所)),即为需要共享的代码(同步代码块,在同步代码块中,相当于是一个单线程,效率低) 2. 共享数据:多个线程共同操作的数据,比如公共厕所就类比共享数据 3. 同步监视器(俗称:锁):任何一个的对象都可以充当锁。(但是为了可读性一般设置英文成lock)当锁住以后只能有一个线程能进去(要求:多个线程必须要共用同一把锁,比如火车上的厕所,同一个标志表示有人) Runable天生共享锁,而Thread中需要用static对象或者this关键字或者当前类(window。class)来充当唯一锁
方式二:同步方法
使用同步方法,对方法进行synchronized关键字修饰 将同步代码块提取出来成为一个方法,用synchronized关键字修饰此方法。 对于runnable接口实现多线程,只需要将同步方法用synchronized修饰 而对于继承自Thread方式,需要将同步方法用static和synchronized修饰,因为对象不唯一(锁不唯一) 总结:
1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
2.非静态的同步方法,同步监视器是this 静态的同步方法,同步监视器是当前类本身。继承自Thread。class
package com.qf;
public class SynchronizedTest extends Thread {
public static void main(String[] args) {
Window window1 = new Window();
Thread t1 = new Thread(window1);
Thread t2 = new Thread(window1);
Thread t3 = new Thread(window1);
t1.setName("售票员1");
t2.setName("售票员2");
t3.setName("售票员3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true){
synchronized (this) {
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"当前售出第"+ticket+"张票");
ticket--;
}else {
break;
}
}
}
}
}
方式三:JDK5.0新增的lock锁方法
package com.qf;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantLockTest {
public static void main(String[] args) {
Window4 window1 = new Window4();
Thread t1 = new Thread(window1);
Thread t2 = new Thread(window1);
Thread t3 = new Thread(window1);
t1.setName("售票员1");
t2.setName("售票员2");
t3.setName("售票员3");
t1.start();
t2.start();
t3.start();
}
}
class Window4 implements Runnable{
private int ticket=100;
private final ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true){
lock.lock();
try {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "当前售出第" + ticket + "张票");
ticket--;
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}
总结:Synchronized与lock的异同?
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的代码逻辑以后,自动的释放同步监视器 lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())(同时以为着lock的方式更为灵活) 优先使用顺序: LOCK-》同步代码块-》同步方法
死锁问题 甲现在要去吃饭他先去拿碗,然后等筷子,乙先去拿筷子,然后等碗,然后两个人就这样一直等着
package com.qf;
public class DeadLockTest {
public static void main(String[] args) {
final String lock1="碗";
final String lock2="筷子";
new Thread("线程1"){
@Override
public void run() {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName()+"拿到了"+lock1+"还差"+lock2);
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"睡眠5ms结束");
synchronized (lock2){
System.out.println(Thread.currentThread().getName()+"获得"+lock2);
}
}
}
}.start();
new Thread("线程2"){
@Override
public void run() {
synchronized (lock2) {
System.out.println(Thread.currentThread().getName()+"拿到了"+lock2+"还差"+lock1);
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"睡眠5ms结束");
synchronized (lock1){
System.out.println(Thread.currentThread().getName()+"获得"+lock1);
}
}
}
}.start();
}
}
运行结果打印
死锁产生的4个必要条件
1、互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。 2、占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。 3、不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。 4、循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
|