? 场景1:班级大扫除,下午4点的开始,小红在扫地,小明在擦窗,小李在擦黑板。下午4点15分结束
? 场景2:班级大扫除,下午4点开始,小红扫了15分钟地,然后擦了15分钟窗,最后擦了5分中黑板。下午4点35分结束。
? 总结:无论是场景1还是场景2,结果都是完成了班级大扫除,但是场景1的三件工作是同时执行的,这就是并行。而场景2是一件事结束然后快速切换到下一件事去执行,这便是并发。
并发:并发指两个或两个以上的事件同一时间间隔发生
操作系统平时的多个进程同时运行并不是真的同时运行,这是一个假象,它实际上是一种并发,多进程运行时操作系统通过快速切换上下文实现的,只不过计算机切换过快,我们用户无感而已。
万事万物都不是凭空出现的,线程也一样,它被创建后的得状态被称为 新建 状态
比如:
Thread thread = new Thread();
线程被创建后是不能使用的,就是让用户在此期间设置一些属性
比如:
// 设置类加载器
thread.setContextClassLoader(System.class.getClassLoader());
// 设置线程名称
thread.setName("商品服务-product-service");
// 是否为守护线程/用户线程
thread.setDaemon(false);
// 设置线程优先级
thread.setPriority(5);
? 通过 thread.start() 方法开启线程,开启后意味着该线程 “能够” 运行,并不意味着一定会运行,因为它要抢占资源,获取CPU的使用权后,才能运行。所以此状态称为 可运行状态。
线程通过努力,获得了CPU的使用权,就会进入执行程序,此时状态被称为 运行状态。
多线程抢占CPU资源,同一时刻仅有一个线程进入临界区,为保证对资源访问的线程安全,同一时刻仅有一个线程进入 synchronized 同步块,而其他未获得访问权的线程将进入 阻塞状态 。
通过调用对象的wait(time)方法或调用线程的sleep(time)/join(time),等待/睡眠指定的时间,此时该线程会进入TIMED_WAITING(sleeping) 状态,直接时间已到,会进入Runnable状态,重新抢占CPU资源。
通过调用对象的wait()方法,让抢占资源的线程等待某工作的完成,或主动join()其他线程,让当前线程释放资源等待被join的线程完成工作,而该线程将进入 等待状态 。
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
小结:
线程状态有 5 种,新建,就绪,运行,阻塞,死亡
下面几个行为,会引起线程阻塞。
- 主动调用 sleep 方法。时间到了会进入就绪状态
- 主动调用 suspend 方法。主动调用 resume 方法,会进入就绪状态
- 调用了阻塞式 IO 方法。调用完成后,会进入就绪状态。
- 试图获取锁。成功的获取锁之后,会进入就绪状态。
- 线程在等待某个通知。其它线程发出通知后,会进入就绪状态
? 在操作系统中,有很多种调度方式,这里介绍分时调度和抢占式调度,JAVA中使用的是抢占式调度,所以主要介绍抢占式调度
分时调度:所有线程轮流使用cpu使用权,平均分配每个CPU的时间
抢占式调度:每个线程都有其优先级,优先让优先级高的进程使用cpu,如果优先级相同,则随机选择执行
? (1)CPU其使用抢占式调度模式在多个线程间进行着高速切换
? (2)对于CPU一个核而言,某个时刻只能进行执行一个线程,而CPU在多个线程间切换速度相对我们比较快,看起来好像“同时”执行
? (3)多线程程序并不能提高程序运行速度,但能提高程序运行效率,让cpu使用率更高。
? Java程序在执行过程中,先启动JVM,并加载对应的class文件,JVM会从main方法开始执行我们的程序代码,一直执行到main方法结束。这个步骤是有一个线程来执行的,这个线程就是主线程。当程序的主线程执行时,如果遇到了循环而导致程序在制定位置停留时间过程,则无法马上执行下面的程序,需要等待循环结束才能往后直行。那么能否实现一个主线程执行循环功能,另一个线程执行其他代码,最终实现多部分代码同时执行的效果呢?多线程便是解决这个问题的。
定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务,因此把run()方法称为执行体。
创建Thread子类的实力,创建了线程对象。
调用新城对象的start()方法来启动该线程
public class FirstThreadTest extends Thread {
int i = 0;
//重写run方法,run方法的方法体就是现场执行体
public void run() {
for (; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
//currentThread() 返回当前线程对象
//getName 返回当前线程名字
System.out.println(Thread.currentThread().getName() + " : " + i);
if (i == 50) {
//创建线程对象并执行start()方法
new FirstThreadTest().start();
new FirstThreadTest().start();
}
}
}
}
分析:
思考:为什么不直接创建Thread对象并start?
Thread t1 = new Thread;
t1.start();
答:以上代码没有语法错误,并不会报错,但是该线程进入运行状态时执行的run方法是Thread类当中的,Thread中的run方法并不是我们实际业务中需要的。Thread 类已经定义了线程任务的编写位置(run 方法),我们只需要继承Thread类并重写一个run方法足够了。
Runnable 接口用来指定每一个线程要执行的任务,包含了一个 run 的无参数抽象方法,需要由接口实现重写该方法。此创建线程的方法是声明实现 Runnable 接口的类,该类实现 run 方法,然后创建 Runnable 的子类对象,传入到某个线程的构造方法中,开启线程
(1) 创建步骤:
定义实现类接口
//定义实现类接口
public class myRunnable implements Runnable {
//重写run方法
public void run()
{
for(int i = 0;i < 5;i++)
{
System.out.println("myRunnable线程正在执行!");
}
}
public static void main(String[] args)
{
//创建线程执行目标类对象
myRunnable mR = new myRunnable();
//将Runnable接口的子类对象作为参数传递给Thread类的构造函数
Thread t1 = new Thread(mR);
Thread t2 = new Thread(mR);
//开启线程
t1.start();
t2.start();
for(int i = 0;i < 5;i++)
{
System.out.println("main线程正在执行!");
}
}
}
3、线程的匿名内部类
使用线程的匿名内部类方式,可以方便的实现每个线程执行不同线程任务操作
方法一:重写 Thread 类中的方法创建线程
new Thread() {
public void run() {
for (int x = 0; x < 40; x++) {
System.out.println(Thread.currentThread().getName()
+ "...X...." + x);
}
}
}.start();
方法二:使用匿名内部类的方式实现 Runnable 接口,重写 Runnable 接口中的 run 方法
Runnable r = new Runnable() {
public void run() {
for (int x = 0; x < 40; x++) {
System.out.println(Thread.currentThread().getName()
+ "...Y...." + x);
}
}
};
new Thread(r).start();
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
1.提供指定线程数量的线程池
2.执行指定的线程的操作(需要提供实现Runnable接口或Callable接口实现类的对象)
3.关闭连接池
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
//3.关闭连接池
service.shutdown();
}
}
说明:
好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没任务时最多保持多长时间后会终止
使用Runnable和Callable方式:
优势:
劣势:
1、编程变复杂了
使用Thread类创建线程方式:
优势:编程简单,易懂
劣势:只能单继承
线程的优先级:
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5 -->默认优先级
2.如何获取和设置当前线程的优先级:
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。
线程通信:wait() / notify() / notifyAll() :此三个方法定义在Object类中的。
? Java程序入口就是由JVM启动main
线程,main
线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。
? 如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。
? 但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:
class TimerThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println(LocalTime.now());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}
}
如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?
? 然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?
? 答案是使用守护线程(Daemon Thread)。
? 守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
? 因此,JVM退出时,不必关心守护线程是否已结束。
? 如何创建守护线程呢?方法和普通线程一样,只是在调用start()
方法前,调用setDaemon(true)
把该线程标记为守护线程:
Thread t = new MyThread();
t.setDaemon(true);
t.start();
在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
? 当某个线程执行的过程中,尚未操作完成时,其他线程参与进来,产生了错误的数据。若让当前线程“睡眠(sleep(long millitime))”的时间越长,出现这类情况的概率往往越大。这就是线程安全问题。
例如在同一个电影院,有三个售票窗口同时卖同一场电影的票(每张电影票上会打印唯一的流水号,以显示卖的是第几张票),若三个售票窗口同时卖票,就有可能会出现同一个流水号的情况。
在Java中,我们通过同步机制,来解决线程的安全问题。
synchronized()同步监视器{
}
说明:
在实现多线程的方法中加上synchronized锁,如
private synchronized void show(){}
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
关于同步方法的总结:
1.实例化ReentrantLock
2.调用锁定方法lock()
3.调用解锁方法:unlock()
代码如下(示例):
class Window implements Runnable{
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
//2.调用锁定方法lock()
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
使用的优先顺序:
Lock —> 同步代码块(已经进入了方法体,分配了相应资源 ) —> 同步方法(在方法体之外)
使用同步方式的利弊:
利:同步的方式,解决了线程的安全问题。
弊:操作同步代码时,只能一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
? JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
public class Counter {
private int count = 0;
public synchronized void add(int n) {
if (n < 0) {
dec(-n);
} else {
count += n;
}
}
public synchronized void dec(int n) {
count += n;
}
}
? 观察synchronized
修饰的add()
方法,当知道到add()
方法内部,当n<0的情况将去调用dec()
方法,将会再去获取到this
锁。
概念:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
说明:出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续。
我们使用同步时,要避免出现死锁。
例如:
public void add(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}
public void dec(int m) {
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
} // 释放lockA的锁
} // 释放lockB的锁
}
在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。对于上述代码,线程1和线程2如果分别执行add()
和dec()
方法时:
add()
,获得lockA
;dec()
,获得lockB
。随后:
lockB
,失败,等待中;lockA
,失败,等待中。此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。
那么我们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致。即严格按照先获取lockA
,再获取lockB
的顺序,改写dec()
方法如下:
public void dec(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
} // 释放lockB的锁
} // 释放lockA的锁
}
? 等待唤醒机制是为了方便处理进程之间通信的手段,多个线程在处理同一个资源时,由于处理的动作(线程的任务)不行同,为了使各个线程能够有效的利用资源,便采取了等待唤醒机制。等待唤醒机制涉及到的方法:
注:
代码实例:
来看一个例子,现有Person类,存储了姓名和年龄,使用 inPut 线程对 Person 类输入信息,使用 outPut 线程对 Person 类获取打印信息
//模拟Person类
public class Person {
String name;
int age;
boolean flag = false;
//输入线程任务inPut类
public class inPut implements Runnable {
private Person p;
int count = 0;
public inPut(Person p) {
this.p = p;
}
public void run() {
while (true)
{
synchronized (p)
{
if(p.flag)
{
try {
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(count % 2 == 0)
{
p.name = "儿童";
p.age = 3;
}
else
{
p.name = "老人";
p.age = 99;
}
p.notify();
p.flag = true;
}
count++;
}
}
}
//输出线程任务outPut类
public class outPut implements Runnable {
private Person p;
public outPut(Person p)
{
this.p = p;
}
public void run() {
while (true)
{
synchronized (p)
{
if(!p.flag)
{
try {
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(p.name + ":" + p.age + "岁");
p.notify();
p.flag = true;
}
}
}
}
//在主线程中调用
public static void main(String[] args)
{
Person P = new Person();
inPut in = new inPut(P);
outPut out = new outPut(P);
Thread T1 = new Thread(in);
Thread T2 = new Thread(out);
T1.start();
T2.start();
}
分析:
生产者消费者问题是一个非常典型性的线程交互的问题。
栈类:
import java.util.ArrayList;
import java.util.LinkedList;
public class MyStack<T> {
LinkedList<T> values = new LinkedList<T>();
public synchronized void push(T t) {
while(values.size()>=200){
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
this.notifyAll();
values.addLast(t);
}
public synchronized T pull() {
while(values.isEmpty()){
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
this.notifyAll();
return values.removeLast();
}
public T peek() {
return values.getLast();
}
}
生产者
public class ProducerThread extends Thread{
private MyStack<Character> stack;
public ProducerThread(MyStack<Character> stack,String name){
super(name);
this.stack =stack;
}
public void run(){
while(true){
char c = randomChar();
System.out.println(this.getName()+" 压入: " + c);
stack.push(c);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public char randomChar(){
return (char) (Math.random()*(‘Z‘+1-‘A‘) + ‘A‘);
}
}
消费者
public class ConsumerThread extends Thread{
private MyStack<Character> stack;
public ConsumerThread(MyStack<Character> stack,String name){
super(name);
this.stack =stack;
}
public void run(){
while(true){
char c = stack.pull();
System.out.println(this.getName()+" 弹出: " + c);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public char randomChar(){
return (char) (Math.random()*(‘Z‘+1-‘A‘) + ‘A‘);
}
}
public class TestThread {
public static void main(String[] args) {
MyStack<Character> stack = new MyStack<>();
new ProducerThread(stack, "Producer1").start();
new ProducerThread(stack, "Producer2").start();
new ConsumerThread(stack, "Consumer1").start();
new ConsumerThread(stack, "Consumer2").start();
new ConsumerThread(stack, "Consumer3").start();
}
}
? 线程池是一种多线程处理形式,在多线程的场景下,频繁的创建线程和结束线程,会造成资源浪费,为了解决这个问题,引入线程池这种设计思想。
线程池思路和生产者消费者模型很相似。
准备一个任务容器
一次性启动10个消费者线程
刚开始任务容器都是空的,所以线程都在wait
当外部线程往这个任务容器中扔了一个任务,就会有一个消费者线程被唤醒notify
这个消费者线程取出任务,并且执行这个任务,执行完毕后,继续等待下一次任务的到来
如果短时间内,有较多的任务进来,那么就有多个线程被唤醒,去执行这些任务。
整个过程中,都不需要创建新的线程,而是循环使用已存在的线程
package multiplethread;
import java.util.LinkedList;
public class ThreadPool {
// 线程池大小
int threadPoolSize;
// 任务容器
LinkedList<Runnable> tasks = new LinkedList<Runnable>();
// 试图消费任务的线程
public ThreadPool() {
threadPoolSize = 10;
// 启动10个任务消费者线程
synchronized (tasks) {
for (int i = 0; i < threadPoolSize; i++) {
new TaskConsumeThread("任务消费者线程 " + i).start();
}
}
}
public void add(Runnable r) {
synchronized (tasks) {
tasks.add(r);
// 唤醒等待的任务消费者线程
tasks.notifyAll();
}
}
class TaskConsumeThread extends Thread {
public TaskConsumeThread(String name) {
super(name);
}
Runnable task;
public void run() {
System.out.println("启动: " + this.getName());
while (true) {
synchronized (tasks) {
while (tasks.isEmpty()) {
try {
tasks.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
task = tasks.removeLast();
// 允许添加任务的线程可以继续添加任务
tasks.notifyAll();
}
System.out.println(this.getName() + " 获取到任务,并执行");
task.run();
}
}
}
}
概念:
所谓原子性操作就是不可中断的操作,比如赋值:int i=5;
原子性本身是线程安全的,但是i--
这个行为是有三个原子性操作组成:
step1:取i的值
step2:i-1
step3:把新的值赋予i
这三步都是线程安全的,但是合在一起,就不安全了。比如:
当余票i只剩1张时i=1
,有两个用户线程A,B来买票,
当A执行第一步和第二步的时候,还没将值i=0
赋予i。
用户B取到了i的值i=1
,这样结果会发生卖出了两张票的情况。
这就是产生线程安全问题的原理。
原文:https://www.cnblogs.com/feixiong1/p/14649365.html