目录
线程,进程,程序的区分
程序(program):为了完成特定的任务,用某种编程语言编写的'一组指令的集合'.即一段静态代码.
进程(process)程序的一次执行过程,或是正在内存中运行的某种程序,程序是静态的,进程是动态的.进程是操作系统调度和分配资源的最小单位
线程(thread):进程的进一步细分,是程序内部的一条执行路径,线程是CPU调度和执行的最小单位
---线程作为CPU调度和执行的最小单元
---一个进程的多个线程共享相同的内存单元,他们从同一个堆中分配对象,可以访问相同的变量和对象,这就使得线程间的通信更加便捷高效但是多个线程操作共享的系统资源可能带来安全隐患
---方法去和堆为线程共享区,虚拟机栈,本地方法栈,程序计数器为线程独享
![]()
并行,并发的区分
并行(parallel)指两个或多个事件在同一时刻发生(同时)指在同一时刻,有多条指令在多个CPU上同时执行.
单核 CPU 无法实现真正的并行处理,因为它只有一个物理处理核心。并行处理需要多个独立的处理核心同时执行不同的任务或指令。
并发(concurrency)指两个或多个事件在同一时间段内发生,即在一段时间内有多条指令在单个CPU上快速轮换交替执行,使得在宏观上具有多个进程同时执行的效果
单核 CPU 不能在硬件层面上实现并行处理,但在软件层面上可以通过一些技术模拟并发操作。例如,使用时间片轮转调度算法,操作系统可以让多个任务交替执行,给人一种并行执行的感觉。这被称为时间分片多任务处理。
另外,还可以通过多线程的方式在单核 CPU 上实现并发操作。多线程是指在一个程序内创建多个线程,每个线程可以独立地执行不同的任务。通过快速切换线程执行的顺序,使得多个任务看起来是同时进行的。
单核CPU的"并行"实际上是并发,多核CPU的"并发"实际上是并行
Thread类的常用方法
构造方法
- public Thread() :分配一个新的线程对象。
- public Thread(string name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :指定创建线程的目标对象,它实现了RunnabTe接口中的run方法- public Thread(Runnable target,string name) :分配一个带有指定目标新的线程对象并指定名字
常用方法
> start() :@启动线程 @调用线程的runO
> run():将线程要执行的操作,声明在runO中。
> currentThread():获取当前执行代码对应的线程
> getName (): 获取线程名
> setName(): 设置线程名
> sTeep(long mills):静态方法,调用时,可以使得当前线程睡眠指定的毫秒数
> yield():静态方法,一旦执行此方法,就释放CPU的执行权
> join(): 在线程a中通过线程b调用join(),意味着线程a进入阻塞状态,直到线程b执行结束,线程a才结束阻寒状态,继续执行。
> isAlive ():判断当前线程是否存活
线程的优先级
getPriority():获取线程的优先级
setPriority():设置线程的优先级,范围[1,10]
默认状态下main线程具有普通优先级(5)
线程创建
线程创建方式一:继承Thread类
创建一个继承Thread类的子类
重写父类里的run()方法,将此线程要执行的操作声明在此方法中
创建当前Thread子类的子类对象
通过对象调用start():1,启动线程2,调用当前线程的run()方法
线程创建方式二:实现Runnable接口
创建一个实现Runnable接口的类
实现接口中的run(),将此线程要执行的操作声明在此方法中
创建当前线程的实现类的对象
将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
Thread类的实例对象调用start():1,启动线程2,调用当前线程的run()方法
线程的生命周期
jkd5.0之前
阻塞是临时状态,死亡时最终状态
jdk5.0之后
Thread有枚举类型的内部类state
jdk5.0之前 | jdk5.0之后 |
就绪 运行 | RUNNABLE |
阻塞 | BLOCKED WAITING TIME-WAITING |
线程安全
一个问题引出线程安全
利用Java写一个三个卖票窗口售票程序
发现售出的票有漏票,重票,的情况,多次执行还发现卖出了第0号票(设置的条件是票号大于0)
★多线程卖票,出现的问题:出现了重票和错票 ★什么原因导致的?线程1操作ticket的过程中,尚未结束的情况下,其他线程也参与进来,对ticket进行操作。 ★如何解决?必须保证一个线程a在操作ticket的过程中,其它线程必须等待,直到线程a操作ticket结束以后,其它线程才可以进来 继续操作ticket。
线程同步机制一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:
> 需要被同步的代码,即为操作共享数据的代码。
> 共享数据:即多个线程都需要操作的数据。比如:ticket
> 需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程中,其它线程必须等待。
> 同步监视器,俗称锁。哪个线程获取了锁,哪个线程就能执行需要被同步的代码。
> 同步监视器,可以使用任何一个类的对象充当。但是,多个线程必须共用同一个同步监视器。
注意:在实现Runnable接口的方式中,同步监视器可以考虑使用:this。
在继承Thread类的方式中,同步监视器要慎用this,可以考虑使用:当前类.class。
同步代码块解决卖票程序的线程安全问题
class SaleTicket implements Runnable{
int ticket = 100;
@Override
public void run() {
synchronized (this){
while(true){
if(ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "SaleTicket,Ticket`s ID is:" + ticket);
ticket--;
}else{
break;
}
}
}
}
}
线程同步机制二:同步方法
> 如果操作共享数据的代码完整的声明在了一个方法中,那么我们就可以将此方法声明为同步方法即可。
> 非静态的同步方法,默认同步监视器是this 静态的同步方法,默认同步监视器是当前类本身。
同步方法解决售票程序的线程安全问题
class SaleTicket implements Runnable{
int ticket = 100;
@Override
public void run() {
//调用同步方法
shoupiao();
}
//同步方法
public synchronized void shoupiao(){
while(true){
if(ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "SaleTicket,Ticket`s ID is:" + ticket);
ticket--;
}else{
break;
}
}
}
}
总结
synchronized
是什么:同步锁的对象可以是任意类型,但是必须保证竞争"同一个共享资源"的多个线程必须使用同一个"同步锁对象"
对于同步代码块来说,同步锁的对象是程序员手动的(很多时候也可以指定为this或类名.class)但是对于同步方法来说,只能是默认的:
静态方法时当前类的Class对象(类名.class)
非静态方法:this
好处:解决了线程的安全问题。
弊端:在操作共享数据时,多线程其实是串行执行的,意味着性能低。
死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
原因 | 如何避免死锁 |
互斥条件 | 互斥条件基本无法被破坏,因为线程需要通过互斥解决安全问题 |
占用且等待 | 可以考虑一次性申请所有所需要的资源,这样就不存在等待的问题 |
不可抢夺 | 占用部分资源的线程在进一步申请其他资源时,如果申请不到,就会主动释放以及占用的资源 |
循环等待 | 可以将资源改为线性顺序.申请资源时先申请序号小的,这样避免循环等待 |
Lock锁
步骤
步骤1. 创建Lock的实例,需要确保多个线程共用同一个Lock实例!需要考虑将此对象声明为static final 步骤2. 执行lock()方法,锁定对共享资源的调用 步骤3. unlock()的调用,释放对共享数据的锁定
Lock锁解决售票系统的线程安全问题
class Window extends Thread{
static int ticket = 100;
//TODO 1. 创建Lock的实例,需要确保多个线程共用同一个Lock实例!需要考虑将此对象声明为static final
private static final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
//TODO 2. 执行lock()方法,锁定对共享资源的调用
lock.lock();
if(ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}finally{
//TODO 3. unlock()的调用,释放对共享数据的锁定
// 为了确保unlock一定会执行,要把它放到finally里
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
Lock锁 与synchronized的对比
synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放对同步监视器的调用。 Lock是通过两个方法控制需要被同步的代码,更灵活一些。 Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。
线程的通信
当我们需要多个线程来共同完成同一件任务的,并且我们希望他们有规律的执行的时候,那么多个线程之间需要一些通信规则,可以协调他们的工作,以此来实现多个线程共同操作一份数据
wait():线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用 notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。(如果被wait()的多个线程的优先级相同,则 随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。 notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
> 此三个方法的使用,必须是在同步代码块或同步方法中。 (超纲:Lock需要配合Condition实现线程间的通信) > 此三个方法的调用者,必须是同步监视器。否则,会报IllegalMonitorStateException异常
这三个方法声明在Object类中
notify
public final void notify()Wakes up a single thread that is waiting on this object's monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. A thread waits on an object's monitor by calling one of the methods.
wait
The awakened thread will not be able to proceed until the current thread relinquishes the lock on this object. The awakened thread will compete in the usual manner with any other threads that might be actively competing to synchronize on this object; for example, the awakened thread enjoys no reliable privilege or disadvantage in being the next thread to lock this object.
This method should only be called by a thread that is the owner of this object's monitor. A thread becomes the owner of the object's monitor in one of three ways:
- By executing a synchronized instance method of that object.
- By executing the body of a statement that synchronizes on the object.
synchronized
- For objects of type by executing a synchronized static method of that class.
Class,
Only one thread at a time can own an object's monitor.
Throws:
IllegalMonitorStateException
- if the current thread is not the owner of this object's monitor.See Also:
notifyAll
public final void notifyAll()Wakes up all threads that are waiting on this object's monitor. A thread waits on an object's monitor by calling one of the methods.
wait
The awakened threads will not be able to proceed until the current thread relinquishes the lock on this object. The awakened threads will compete in the usual manner with any other threads that might be actively competing to synchronize on this object; for example, the awakened threads enjoy no reliable privilege or disadvantage in being the next thread to lock this object.
This method should only be called by a thread that is the owner of this object's monitor. See the method for a description of the ways in which a thread can become the owner of a monitor.
notify
Throws:
IllegalMonitorStateException
- if the current thread is not the owner of this object's monitor.See Also:
wait
public final void wait() throws InterruptedExceptionCauses the current thread to wait until it is awakened, typically by being notified or interrupted.
In all respects, this method behaves as if had been called. See the specification of the
wait(long, int)
method for details.wait(0L, 0)
Throws:
IllegalMonitorStateException
- if the current thread is not the owner of the object's monitor
InterruptedException
- if any thread interrupted the current thread before or while the current thread was waiting. The interrupted status of the current thread is cleared when this exception is thrown.See Also:
callable
线程创建方式三,实现callable接口
//TODO 1.创建一个实现Callable的实现类
class NumThread implements Callable {
//TODO 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;
}
Thread.sleep(1000);
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) {
//TODO 3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//TODO 4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//TODO 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
Thread t1 = new Thread(futureTask);
t1.start();
System.out.println("main()线程");
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (Exception e) {
e.printStackTrace();
}
}
}
与之前处理方式(Runnable)对比
> call()可以有返回值,更灵活
>call()可以使用throws的方式处理异常
>Callable使用了泛型参数,可以指明具体的call()函数的返回值类型
>如果在主线程中需要获取分线程call()的返回值,则此时的主线程时阻塞状态
线程池的使用
线程创建方式四,使用线程池
//创建并使用多线程的第四种方法:使用线程池
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());//ThreadPoolExecutor
service1.setMaximumPoolSize(50); //设置线程池中线程数的上限
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
//3.关闭连接池
service.shutdown();
}
}
使用线程池的好处
> 提高了程序执行的效率。(因为线程已经提前创建好了) > 提高了资源的复用率。(因为执行完的线程并未销毁,而是可以继续执行其他的任务) > 可以设置相关的参数,对线程池中的线程的使用进行管理