JavaSE学习笔记(12)---线程

多线程

并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

    简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

线程调度:

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

    • 设置线程的优先级

    • 抢占式调度详解

      大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

      实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。

      其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

创建线程类

第一种: 通过继承Thread类实现多线程

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该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("main线程!"+i);
}
}
}

自定义线程类:

public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}

第二种: 通过Runnable接口实现多线程

在开发中,我们应用更多的是通过Runnable接口实现多线程。这种方式克服了11.2.1节中实现线程类的缺点,即在实现Runnable接口的同时还可以继承某个类。所以实现Runnable接口的方式要通用一些。

通过Runnable接口实现多线程

public class TestThread2 implements Runnable {//自定义类实现Runnable接口;
//run()方法里是线程体;
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
//创建线程对象,把实现了Runnable接口的对象作为参数传入;
Thread thread1 = new Thread(new TestThread2());
thread1.start();//启动线程;
Thread thread2 = new Thread(new TestThread2());
thread2.start();
}
}

线程状态

一个线程对象在它的生命周期内,需要经历5个状态。

▪ 新生状态(New)

用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。

▪ 就绪状态(Runnable)

处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。有4中原因会导致线程进入就绪状态:

\1. 新建线程:调用start()方法,进入就绪状态;

\2. 阻塞线程:阻塞解除,进入就绪状态;

\3. 运行线程:调用yield()方法,直接进入就绪状态;

\4. 运行线程:JVM将CPU资源从本线程切换到其他线程。

▪ 运行状态(Running)

在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。

▪ 阻塞状态(Blocked)

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有4种原因会导致阻塞:

\1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。

\2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。

\3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。

\4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。

▪ 死亡状态(Terminated)

死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop()/destroy()方法已经被JDK废弃,不推荐使用)。

当一个线程进入死亡状态以后,就不能再回到其它状态了。

终止线程我们一般不使用JDK提供的stop()/destroy()方法(它们本身也被JDK废弃了)。通常的做法是提供一个boolean型的终止变量,当这个变量置为false,则终止线程的运行。

终止线程的典型方法(重要)

public class TestThreadCiycle implements Runnable {
String name;
boolean live = true;// 标记变量,表示线程是否可中止;
public TestThreadCiycle(String name) {
super();
this.name = name;
}
public void run() {
int i = 0;
//当live的值是true时,继续线程体;false则结束循环,继而终止线程体;
while (live) {
System.out.println(name + (i++));
}
}
public void terminate() {
live = false;
} public static void main(String[] args) {
TestThreadCiycle ttc = new TestThreadCiycle("线程A:");
Thread t1 = new Thread(ttc);// 新生状态
t1.start();// 就绪状态
for (int i = 0; i < 100; i++) {
System.out.println("主线程" + i);
}
ttc.terminate();
System.out.println("ttc stop!");
}
}

执行结果如图所示:

运行效果图(因为是多线程,故每次运行结果不一定一致)

暂停线程执行sleep/yield

暂停线程执行常用的方法有sleep()和yield()方法,这两个方法的区别是:

  1. sleep()方法:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。

  2. yield()方法:可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。

暂停线程的方法-sleep()

public class TestThreadState {
public static void main(String[] args) {
StateThread thread1 = new StateThread();
thread1.start();
StateThread thread2 = new StateThread();
thread2.start();
}
}
//使用继承方式实现多线程
class StateThread extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + ":" + i);
try {
Thread.sleep(2000);//调用线程的sleep()方法;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

执行结果如下所示(注:以下图示只是部分结果,运行时可以感受到每条结果输出之前的延迟,是Thread.sleep(2000)语句在起作用):

图11-6示例11-4运行效果图

暂停线程的方法-yield()

public class TestThreadState {
public static void main(String[] args) {
StateThread thread1 = new StateThread();
thread1.start();
StateThread thread2 = new StateThread();
thread2.start();
}
}
//使用继承方式实现多线程
class StateThread extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + ":" + i);
Thread.yield();//调用线程的yield()方法;
}
}
}

执行结果如图所示(注:以下图示只是部分结果,可以引起线程切换,但运行时没有明显延迟):

运行效果图

线程的联合join()

线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。如下面示例中,“爸爸线程”要抽烟,于是联合了“儿子线程”去买烟,必须等待“儿子线程”买烟完毕,“爸爸线程”才能继续抽烟。

public class TestThreadState {
public static void main(String[] args) {
System.out.println("爸爸和儿子买烟故事");
Thread father = new Thread(new FatherThread());
father.start();
}
} class FatherThread implements Runnable {
public void run() {
System.out.println("爸爸想抽烟,发现烟抽完了");
System.out.println("爸爸让儿子去买包红塔山");
Thread son = new Thread(new SonThread());
son.start();
System.out.println("爸爸等儿子买烟回来");
try {
son.join();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("爸爸出门去找儿子跑哪去了");
// 结束JVM。如果是0则表示正常结束;如果是非0则表示非正常结束
System.exit(1);
}
System.out.println("爸爸高兴的接过烟开始抽,并把零钱给了儿子");
}
} class SonThread implements Runnable {
public void run() {
System.out.println("儿子出门去买烟");
System.out.println("儿子买烟需要10分钟");
try {
for (int i = 1; i <= 10; i++) {
System.out.println("第" + i + "分钟");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("儿子买烟回来了");
}
}

线程的优先级

  1. 处于就绪状态的线程,会进入“就绪队列”等待JVM来挑选。

  2. 线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5。

  3. 使用下列方法获得或设置线程对象的优先级。

    int getPriority();

void setPriority(int newPriority);

注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高的线程后调用优先级低的线程。

什么是线程同步

▪ 同步问题的提出

现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题。 比如:教室里,只有一台电脑,多个人都想使用。天然的解决办法就是,在电脑旁边,大家排队。前一人使用完后,后一人再使用。

▪ 线程同步的概念

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的这种问题。

由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized 方法和 synchronized 块。

▪ synchronized 方法

通过在方法声明中加入 synchronized关键字来声明,语法如下:

public  synchronized  void accessVal(int newVal);

synchronized 方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。

▪ synchronized块

synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。

Java 为我们提供了更好的解决办法,那就是 synchronized 块。 块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。

synchronized 块:通过 synchronized关键字来声明synchronized 块,语法如下:

synchronized(syncObject)
  {
   //允许访问控制的代码
  }

多线程操作同一个对象(使用线程同步)

public class TestSync {
public static void main(String[] args) {
Account a1 = new Account(100, "高");
Drawing draw1 = new Drawing(80, a1);
Drawing draw2 = new Drawing(80, a1);
draw1.start(); // 你取钱
draw2.start(); // 你老婆取钱
}
}
/*
* 简单表示银行账户
*/
class Account {
int money;
String aname;
public Account(int money, String aname) {
super();
this.money = money;
this.aname = aname;
}
}
/**
* 模拟提款操作
*
* @author Administrator
*
*/
class Drawing extends Thread {
int drawingNum; // 取多少钱
Account account; // 要取钱的账户
int expenseTotal; // 总共取的钱数 public Drawing(int drawingNum, Account account) {
super();
this.drawingNum = drawingNum;
this.account = account;
} @Override
public void run() {
draw();
} void draw() {
synchronized (account) {
if (account.money - drawingNum < 0) {
System.out.println(this.getName() + "取款,余额不足!");
return;
}
try {
Thread.sleep(1000); // 判断完后阻塞。其他线程开始运行。
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money -= drawingNum;
expenseTotal += drawingNum;
}
System.out.println(this.getName() + "--账户余额:" + account.money);
System.out.println(this.getName() + "--总共取了:" + expenseTotal);
}
}

synchronized (account)” 意味着线程需要获得account对象的“锁”才有资格运行同步块中的代码。 Account对象的“锁”也称为“互斥锁”,在同一时刻只能被一个线程使用。A线程拥有锁,则可以调用“同步块”中的代码;B线程没有锁,则进入account对象的“锁池队列”等待,直到A线程使用完毕释放了account对象的锁,B线程得到锁才可以开始调用“同步块”中的代码。

JavaSE学习笔记(12)---线程的更多相关文章

  1. python学习笔记12 ----线程、进程

    进程和线程的概念 进程和线程是操作系统中两个很重要的概念,对于一般的程序,可能有若干个进程,每一个进程有若干个同时执行的线程.进程是资源管理的最小单位,线程是程序执行的最小单位(线程可共享同一进程里的 ...

  2. JavaSE学习笔记(13)---线程池、Lambda表达式

    JavaSE学习笔记(13)---线程池.Lambda表达式 1.等待唤醒机制 线程间通信 概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同. 比如:线程A用来生成包子的,线程B用 ...

  3. JavaSE学习笔记(2)---面向对象基础

    JavaSE学习笔记(2)---面向对象基础 1.面向对象具有三大特征:封装性.继承性和多态性,而面向过程没有继承性和多态性,并且面向过程的封装只是封装功能,而面向对象可以封装数据和功能.所以面向对象 ...

  4. 并发编程学习笔记(12)----Fork/Join框架

    1. Fork/Join 的概念 Fork指的是将系统进程分成多个执行分支(线程),Join即是等待,当fork()方法创建了多个线程之后,需要等待这些分支执行完毕之后,才能得到最终的结果,因此joi ...

  5. SpringMVC:学习笔记(12)——ThreadLocal实现会话共享

    SpringMVC:学习笔记(12)——ThreadLocal实现会话共享 ThreadLocal ThreadLocal,被称为线程局部变量.在并发编程的情况下,使用ThreadLocal创建的变量 ...

  6. javaSE学习笔记(17)---锁

    javaSE学习笔记(17)---锁 Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率.本文旨在对锁相关源码(本文中的源码来自JDK 8).使用场景进行举例,为读 ...

  7. javaSE学习笔记(16)---网络编程

    javaSE学习笔记(16)---网络编程 基本概念 如今,计算机已经成为人们学习.工作.生活必不可少的工具.我们利用计算机可以和亲朋好友网上聊天,也可以玩网游.发邮件等等,这些功能实现都离不开计算机 ...

  8. javaSE学习笔记(10)---List、Set

    javaSE学习笔记(10)---List.Set 1.数据存储的数据结构 常见的数据结构 数据存储的常用结构有:栈.队列.数组.链表和红黑树. 1.栈 栈:stack,又称堆栈,它是运算受限的线性表 ...

  9. JavaSE学习笔记(9)---集合类和泛型

    JavaSE学习笔记(9)---集合类和泛型 1.Collection集合 集合概述 在前面我们已经学习过并使用过集合ArrayList<E> ,那么集合到底是什么呢? 集合:集合是jav ...

随机推荐

  1. hashlib 模块的用法

    import hashlib #多用于加密a=hashlib.md5()print(a) #<md5 HASH object @ 0x00000000021CCF90> a.update( ...

  2. Linux学习笔记-配置阿里云yum源

    进入目录:cd /etc/yum.repos.d 备份:mkdir repobak mv *.repo repobak/ 下载CentOS-Base.repo 到路径/etc/yum.repos.d/ ...

  3. Android View如何获取焦点

    Android新启动Activity,dialog或者其他窗体中中包含EditText, 新启动的activity的焦点默认在EditText上,这是android系统会弹出软键盘,挤压activit ...

  4. MySQL初次安装配置及修改密码

    安装前的准备工作: 下载完后,我们将 zip 包解压到相应的目录,这里我将解压后的文件夹放在 C:\mysql-8.0.11 下. 接下来我们需要配置下 MySQL 的配置文件 打开刚刚解压的文件夹 ...

  5. ARTS Week 9

    Dec 23, 2019 ~ Dec 29, 2019 Algorithm Problem 69 Sqrt(x) 实现求解平方根函数Sqrt(x) 题目链接 题目描述:给定一个非负数x,求解该数字的平 ...

  6. Java1变量数据类型和运算符

    day02_变量数据类型和运算符   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class jh_01_数据类型说明 {     public  ...

  7. How to setup backup by using EMC NW + EMC NMM for sqlserver failover cluster (not always on)

    As we said, sqlsever fail over cluster is perviously version of always on. The HA was guarenteed by ...

  8. 检测并移除WMI持久化后门

      WMI型后门只能由具有管理员权限的用户运行.WMI后门通常使用powershell编写的,可以直接从新的WMI属性中读取和执行后门代码,给代码加密.通过这种方式攻击者会在系统中安装一个持久性的后门 ...

  9. Go语言实现:【剑指offer】旋转数组的最小数字

    该题目来源于牛客网<剑指offer>专题. 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转. 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素. 例如数组{3, ...

  10. PHP常见代码执行后门函数

    PHP常见代码执行后门函数 代码执行: 应用程序在调用一些能够将字符串转换为代码的函数(例如php中的eval中),没有考虑用户是否控制这个字符串,将造成代码执行漏洞. 常见php代码执行函数介绍 常 ...