文章目录
- 多线程
- 线程
- 线程安全
- 线程状态
- 线程间通信
- 线程池
多线程
- 并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
对于单核CPU系统中,我们可以同时运行多个程序,比如一边编辑记事本,一边听音乐,一边录屏。这些在宏观上是并行的,但是在微观上是并发的。
- 线程与进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创
建、运行到消亡的过程。 - 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
线程调度:
- 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
- 创建线程类
- 定义Thread类的子类,并重写该类的run()方法。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
public class CreateThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
public static void main(String[] args) {
CreateThread createThread = new CreateThread();
createThread.start();
System.out.println("over");
}
}
线程
每一个执行线程都有一片自己所属的栈内存空间
- Thread类
构造方法:
public Thread()
:分配一个新的线程对象。public Thread(String name)
:分配一个指定名字的新的线程对象。public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字。
代码示例:
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = new Thread();
Thread thread1 = new Thread("小明线程");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是Runnable接口创建的线程");
}
});
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是Runnable接口创建的线程,并且还设置了名字");
}
}, "小明线程2");
System.out.println(thread.getName());
System.out.println(thread1.getName());
System.out.println(thread2.getName());
System.out.println(thread3.getName());
}
}
常用方法:
public String getName()
:获取当前线程名称。public void start()
:导致此线程开始执行; Java虚拟机调用此线程的run方法。public void run()
:此线程要执行的任务在此处定义代码。public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。public static Thread currentThread()
:返回对当前正在执行的线程对象的引用。
代码示例
public class ThreadDemo2 extends Thread{
public ThreadDemo2(String name) {
super(name);
}
@Override
public void run() {
System.out.println(Thread.currentThread());
}
public static void main(String[] args) throws InterruptedException {
ThreadDemo2 thread = new ThreadDemo2("小明线程");
System.out.println(thread.getName());
thread.start();
long start = System.currentTimeMillis();
Thread.sleep(10000);
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
thread.run();
}
}
这里我们可以看到当程序执行的时候,直接输出了小明线程、以及线程名为小明线程的对象引用,然后再执行的时候主线程因为调用了 Thread.sleep(10000) 方法暂停了10s,在10s过后打印了一共消耗的时间以及主线程的线程对象引用。
- 创建线程的俩种方式以及其区别
- 一种是继承Thread类方式
public class ThreadDemo2 extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread());
}
public static void main(String[] args) throws InterruptedException {
ThreadDemo2 thread = new ThreadDemo2();
System.out.println(thread.getName());
}
}
- 一种是实现Runnable接口方式
public class ThreadDemo2 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread());
}
public static void main(String[] args) throws InterruptedException {
ThreadDemo2 threadDemo2 = new ThreadDemo2();
Thread thread = new Thread(threadDemo2);
}
}
区别:
- 如果类继承Thread,则不适合资源共享,但是类实现了Runable接口的话,则很容易的实现资源共享。
- 实现Runnable接口适合多个相同的程序代码的线程去共享同一个资源。
- 实现Runnable接口可以避免java中的单继承的局限性。
- 实现Runnable接口增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
注意:
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用
java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程
线程安全
多个线程同时运行,当这些线程同时运行相同的代码。程序每次运行结果和单线程运行的结果是一样 的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
- 线程同步
通过电影票买票操作进行线程的讲解,假设总共有100张票,分别使用三个窗口进行卖票,一直到票卖光为止。
public class Ticket implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
class Demo {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
//同时卖票
t1.start();
t2.start();
t3.start();
}
}
这里我们可以看到,有三个窗口卖同一张票的情况,也有票买完了还在卖的情况,这里就出现了线程不安全的情况。
保证线程安全的三种方法
- 同步代码块: synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
public class Ticket implements Runnable {
private int ticket = 100;
Object lock = new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
}
class Demo {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
//同时卖票
t1.start();
t2.start();
t3.start();
}
}
这里我们使用 对象锁 的方式来实现同步锁,可以看到当我们有线程拿到了同步锁之后,其他线程不能对公共资源进行访问了,只能进行等候,直到当前线程释放掉同步锁。
- 同步方法:使用synchronized修饰的方法
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
package demo5;
public class Ticket implements Runnable {
private int ticket = 100;
@Override
public synchronized void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
class Demo {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
//同时卖票
t1.start();
t2.start();
t3.start();
}
}
这里的同步锁,对于非静态方法来说,同步锁就是this,对于静态方法来说同步锁就是当前方法使用所在类的字节码对象(类名.class)
- 锁机制:Lock锁也称同步锁,加锁与释放锁方法化
更加符合面向对象的思想
package demo5;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable {
private int ticket = 100;
Lock lock = new ReentrantLock();
@Override
public synchronized void run() {
while (true) {
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
lock.unlock();
}
}
}
class Demo {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
//同时卖票
t1.start();
t2.start();
t3.start();
}
}
通过多态的思想创建了一个Lock对象,通过 lock.lock() 方法对存在线程安全代码块进行上锁,通过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方法而死亡。 |
线程间通信
多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
- 等待唤醒机制
通过等待唤醒机制来保证线程间通信有效利用资源
在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
- wait:线程不再活动,不再参与调度。
- notify:则选取所通知对象的 wait set 中的一个线程释放;
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
线程池
是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
代码示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " + Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教练回到了游泳池");
}
}
class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(5);
MyRunnable r = new MyRunnable();
// 从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);
// 再获取个线程对象,调用MyRunnable中的run()
service.submit(r);
service.submit(r);
service.submit(r);
service.submit(r);
// 关闭线程池
//service.shutdown();
}
}
这里我们首先通过java.util.concurrent.Executors类中的静态方法 newFixedThreadPool 创建了线程池对象,并且指定了对应的线程数,通过 ExecutorService对象调用 Future<?> submit(Runnable task) 方法使用了线程池中的线程对象。
注意:
submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中。
欢迎java热爱者了解文章,作者将会持续更新中,期待各位友友的关注和收藏。。。