线程(一)
线程
第一章 多线程
学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那么现在详设计一个程序,边打游戏边听歌,要解决这个问题,就需要使用多进程或多线程
1.1并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生。
注意:单核处理器的计算机肯定是不能并行处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
1.2线程与进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
进入到内存的程序叫进程
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
应用程序到CPU的执行路径叫做线程
tips:一个程序运行后至少有一个进程,一个进程可以包含多个线程
线程调度
分时调度
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
抢先式调度
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
主线程
执行main方法的线程
单线程程序:java程序中只有一个线程
执行从main方法开始,从上到下依次执行
package Demo04;
public class Demo01MainThread {
public static void main(String[] args) {
Person p1=new Person("小明");
p1.run();
Person p2=new Person("小林");
p2.run();
}
}
package Demo04;
public class Person {
private String name;
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println(name+"--->"+i);
}
}
public Person() {
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
1.3创建线程方式一
Java使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。
第一种方法:Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
void start()使该线程开始执行;java虚拟机调用该线程的run方法。结果是两个线程并发的运行,当前线程(main方法)和另一个线程(创建的新线程,执行其run方法)。多次启动一个线程是非法的,特别是当前线程已经结束执行后,不能再重新启动,java程序属于抢占式调度,哪个线程的优先级高,哪个线程就优先执行,同一优先级,随机选择一个执行。
package Demo04;
public class Demo02Thread {
public static void main(String[] args) {
MyThread mt=new MyThread();
mt.start();
for (int i = 0; i < 20; i++) {
System.out.println("main:"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run:"+i);
}
}
}
第二章 线程
2.1多线程原理
1.随机性打印结果
2.多线程内存图解
2.2Thread类
构造方法:
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()
:返回对当前正在执行的线程对象的引用
1.获取当前线程名称
- 使用Thread类中的方法getName()
String getName()返回该线程的名称。 - 可以先获取到当前正在执行的线程,使用线程中的方法getName()获取线程的名称
static Thread currentThread()返回对当前正在执行的线程对象的引用。
package Demo04;
public class Demo03Thread2 extends Thread {
public static void main(String[] args) {
MyThread2 mt=new MyThread2();
mt.start();//Thread-0
}
}
class MyThread2 extends Thread {
@Override
public void run() {
//第一种方法
// String name = getName();
// System.out.println(name);
//第二种方法
// Thread thread = Thread.currentThread();
// String name = thread.getName();
// System.out.println(name);
//链式编程
System.out.println(Thread.currentThread().getName());
}
}
2.设置线程的名称:(了解)
- 使用Thread类中的方法setName(名字)
void setName ( String name)改变线程名称,使之与参数name 相同。 - 创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread)给子线程起一个名字
Thread ( String name)分配新的 Thread 对象。
3.sleep
public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。毫秒数结束后,线程继续执行。
package Demo04;
public class Demo04Sleep {
public static void main(String[] args) {
//模拟秒表
for (int i = 1; i <= 60; i++) {
System.out.println(i);
//使用Thread类的sleep方法让程序睡眠1秒钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.3创建线程方式二
采用java.lang.Runnable
,我们只需要重写run方法即可。
步骤:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
package com.indi.demo04;
public class Demo05Runnable {
public static void main(String[] args) {
RunnableImpl run =new RunnableImpl();
Thread t =new Thread(run);
t.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
class RunnableImpl implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
2.4Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runnable接口的话,则很容易实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
1.适合多个相同的程序代码的线程去共享同一个资源。
2.可以避免Java中的单继承的局限性。
3.增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
4.线程池只能放入实现Runnable或Callable类线程,不能直接放入继承Thread的类。
扩充:在Java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用Java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实就是在操作系统中启动了一个进程。
2.5匿名内部类方式实现线程的创建
package com.indi.demo04;
public class Demo06InnerClassThread {
public static void main(String[] args) {
//线程的父类是Thread
//new MyThread().start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}.start();
//线程的接口Runnable
//Runnable r =new RunnableImpl();//多态
Runnable t=new Runnable(){
//重写run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
};
new Thread(t).start();
//简化接口的方式
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}).start();
}
}
第三章 线程安全
3.1线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
我们通过一个案例,演示线程的安全问题:
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是”“战狼三”,本次电影的座位共100个(本场电影只能卖100张票)。
我们来模拟电影院的售票窗口,实现多个窗口同时卖“葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)
需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟
模拟票︰
package com.indi.demo06.ThreadSafe;
public class Demo01Ticket {
public static void main(String[] args) {
RunnableImpl mt =new RunnableImpl();
Thread t0 =new Thread(mt);
Thread t1 =new Thread(mt);
Thread t2 =new Thread(mt);
t0.start();
t1.start();
t2.start();
}
}
package com.indi.demo07.Synchronized;
/*
解决线程安全问题的第一种方案:使用同步代码块
*/
public class RunnableImpl implements Runnable {
//定义一个多个线程共享的票源
private int ticket=100;
// 设置线程任务:卖票
@Override
public void run() {
// 使用死循环,让卖票操作重复执行
while (true){
// 先判断票是否存在
if (ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
卖票案例出现了线程安全问题,卖出了不存在的票和重复的票。
3.2线程同步
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java提供了同步机制(synchronized)来解决。
根据案例简述︰
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个钱程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
有三种方式完成同步操作:
- 同步代码块
- 同步方法
- 锁机制
3.3同步代码块
- 同步代码块:
synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码(访问了共享数据的代码)
}
同步锁
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。
1.锁对象 可以是任意类型。
2.多个线程对象 要使用同一把锁。
3.锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就能进入代码块,其他的线程只能在外等着(BLOCKED)
package com.indi.demo07.Synchronized;
public class Demo01Ticket {
public static void main(String[] args) {
RunnableImpl mt =new RunnableImpl();
Thread t0 =new Thread(mt);
Thread t1 =new Thread(mt);
Thread t2 =new Thread(mt);
t0.start();
t1.start();
t2.start();
}
}
package com.indi.demo07.Synchronized;
/*
解决线程安全问题的第一种方案:使用同步代码块
*/
public class RunnableImpl implements Runnable {
//定义一个多个线程共享的票源
private int ticket=100;
Object obj = new Object();
// 设置线程任务:卖票
@Override
public void run() {
// 使用死循环,让卖票操作重复执行
while (true){
synchronized (obj){
// 先判断票是否存在
if (ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
}
原理
3.4同步方法
- 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步锁是Who?
- 对于非static方法,同步锁就是this
- 对于static方法,静态方法的锁对象是本类的class属性—>calss文件对象(反射)
1.非静态方法
package com.indi.demo08.Synchronized;
public class Demo01Ticket {
public static void main(String[] args) {
RunnableImpl mt =new RunnableImpl();
Thread t0 =new Thread(mt);
Thread t1 =new Thread(mt);
Thread t2 =new Thread(mt);
t0.start();
t1.start();
t2.start();
}
}
package com.indi.demo08.Synchronized;
/*
解决线程安全问题的第二种方案:使用同步方法
步骤:
1.把访问了共享数据的代码抽取出来,放到一个方法中
2.在方法上添加synchronized修饰符
*/
public class RunnableImpl implements Runnable {
private int ticket=100;
@Override
public void run() {
while (true){
payTicket();
}
}
public synchronized void payTicket(){
if (ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->正在卖第"+ticket+"张票");
ticket--;
}
}
}
2.静态方法
package com.indi.demo08.Synchronized;
public class Demo01Ticket {
public static void main(String[] args) {
RunnableImpl mt =new RunnableImpl();
Thread t0 =new Thread(mt);
Thread t1 =new Thread(mt);
Thread t2 =new Thread(mt);
t0.start();
t1.start();
t2.start();
}
}
package com.indi.demo08.Synchronized;
/*
解决线程安全问题的第二种方案:使用同步方法
步骤:
1.把访问了共享数据的代码抽取出来,放到一个方法中
2.在方法上添加synchronized修饰符
*/
public class RunnableImpl implements Runnable {
private static int ticket=100;
@Override
public void run() {
while (true){
payTicket();
}
}
public static void payTicket(){
synchronized(RunnableImpl.class){
if (ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
3.5Lock锁
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法:
public void lock()
:加同步锁public void unlock()
:释放同步锁
java.util.concurrent.locks.ReentrantLock impLements Lock接口
使用步骤:
- 在成员位置创建一个ReentrantLock对象
- 在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁
- 在可能会出现安全问题的代码后调用Lock接口中的方法unLock释放锁
package com.indi.demo09.Lock;
public class Demo01Ticket {
public static void main(String[] args) {
RunnableImpl mt =new RunnableImpl();
Thread t0 =new Thread(mt);
Thread t1 =new Thread(mt);
Thread t2 =new Thread(mt);
t0.start();
t1.start();
t2.start();
}
}
package com.indi.demo09.Lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
* 解决线程安全问题的第三种方案:使用Lock锁
*
* */
public class RunnableImpl implements Runnable {
private int ticket=100;
//1.在成员位置创建一个ReentrantLock对象
Lock l =new ReentrantLock();
@Override
public void run() {
while (true){
l.lock();
if (ticket>0){
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"--->正在卖第"+ticket+"张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
l.unlock();//无论程序是否异常,都会把锁释放
}
}
}
}
}
- 本文作者: ShanTianQi
- 本文链接: https://stq555.com/2022/01/09/%E7%BA%BF%E7%A8%8B-%E4%B8%80/index.html
- 版权声明:本博客所有文章均采用 BY-NC-SA 许可协议,转载请注明出处!