线程与进程
进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程(至少有一个)。
线程:线程是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
JAVA天生就是多线程程序,默认至少有两个线程main gc
进程和线程的区别
根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:系统为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
线程的状态
线程的状态:
可以分为4个阶段:新生状态、可运行状态、阻塞状态、死亡状态。线程总是处于这四种状态之一
1.新生状态(New Thread):
创建线程对象后,尚未调用其start()方法之前,这个线程就有了生命,此时线程仅仅是一个空对象,系统美哟有为其分配资源
此时线程只有启动和终止,任何其他操作都会引发异常
2.可运行状态(Runnable)
当调用start()方法启动线程之后,系统为该线程分配CPU外的所需资源,这个先春风就有了运行的机会,线程处于可运行状态,在这个状态中,该
线程对象可能正在运行,也可能尚未运行。对于只有一个cpu的机器而言,任何时刻之恶能有处于可运行状态的线程占有处理机
获得cpu资源,此时系统正真运行线程的run方法
3.阻塞状态(Blocked)
正在运行的线程,因为某种原因,不能继续执行,进入阻塞状态
这种状态是一种"不可运行状态",但是这种状态在得到一个特定事件后,转会可运行状态
导致一个线程被阻塞的原因:
1.调用了Thread类的静态方法sleep()
2.如果一个线程执行需要得到一个对象的锁正在被别的线程占用,那么此线程会被阻塞
3.线程的suspend()方法被调用,而使线程被挂起,线程进入到阻塞状态,但是这个方法不用了,因为这个方法容易产生死锁,被jdk列为过期方法。
4.一个线程执行i/o流操作时,如果I/O操作尚未完成,则线程发生阻塞
处于阻塞状态的线程可以转回可运行状态,例如,在调用sleep()方法之后,这个线程的睡眠时间已经达到了指定的间隔,那么它就有可能重新回到可运行状态。
或当一个线程等待的锁变得可用时候,那么这个线程也会从被阻塞状态转入可运行状态。
4.死亡状态
一个线程run()运行完毕、stop()方法被调用,或者在运行过程出现未捕获异常时,线程进入死亡
5.线程调度:
线程调度是抢占式调度,即在当前线程执行过程中如果有一个更高优先级的线程进入可运行状态,则这个更高优先级的线程立即被调度执行。
线程优先级别
线程默认级别为5,级别范围为1-10。线程的优先级可以通过 thread.setPriority(int grade);方法进行修改,此方法的参数表示要设值的优先级,它必须是一个1~10的整数。
实现线程调度的方法
1.join()方法使当前线程暂停,等待调用该方法的线程执行结束后,再继续执行本线程
2.sleep()
public static void sleep(long mills)
sleep()方法会让当前线程睡眠(停止执行)毫秒,线程由运行的状态进入不可运行的状态
,睡眠事件过后,线程会再次进入可运行状态
3.yield()
public static void yield()
yield()方法可以使当前线程暂停执行,允许其他线程执行,但该线程仍然处于可运行状态,并不变为阻塞状态。此时
系统选择其他相同的或优先级别更高的线程执行,若无其他相同或更高优先线程,则该线程继续执行
总结:sleep()和yield()有什么区别?
sleep:
1·使当前线程被阻塞状态;
2.即使没有其他等待执行的线程,当前线程也会等待指定的时间
3.其他等待线程的机会是均等的
yield():
1. 将当前线程转入暂停执行的状态
2.如果没有其他等待执行的线程,当前线程会马上恢复执行
3.会运行优先级相同或更高的线程
实例锁:
类声明后,我们可以new出来很多实例对象,这个时候,每个实例在jvm中都有字节的引用地址和堆内存空间
我们就可以认为这些实例都是独立的个体,很显然,在实例上加锁和其他的实例就没有关系,互不影响
1.锁住实体类中非静态的变量
2.锁住this变量
this指当前对象的本身,所以使用synchronized(this)方式的方法都共享一把锁
3.锁住非静态方法
类锁
类锁指的是加载类上,而类的信息存在jvm方法去,并且jvm只有一份,方法区又是线程共享的,所以类锁是线程共享
总结:
1.使用对象锁的情况,只有使用同一实例的线程才会受锁的影响
2.类锁是所有线程共享的锁,所以同一时刻,只能有一个线程使用了锁的方法或者或方法体,不管是不是同一个实例。
实现线程间的通信
Java中提供了如下3个方法实现线程的通信
wait():调用wait()方法会挂起当前线程,并释放共享资源锁
notify():调用任意对象的notify()方法会因调用该对象的wait()方法而阻塞的线程中随机选择一个线程接触阻塞,但要等到获得锁对象后才真正的去执行。
notifyAll()方法:调用notifyAll方法会将因调用该对象的wait()方法而阻塞的所有线程。
一次性全部解除阻塞。
注意:
wait()、notify()、notifyAll()这3个方法都是Object类中用final修饰的方法 ,被所有类继承且
不允许重写,这3个方法只能在同步方法或者同步代码块中使用,否则会抛异常。
实现线程
继承Thread类
编写一个类,直接 继承 java.lang.Thread,重写 run方法。
代码:
// 定义线程类
public class MyThread extends Thread{
public void run(){
}
}
// 创建线程对象
MyThread t = new MyThread();
// 启动线程。
t.start();
eg.
public class ThreadTest02 {
public static void main(String[] args) {
MyThread t = new MyThread();
// 启动线程
//t.run(); // 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)
t.start();
// 这里的代码还是运行在主线程中。
for(int i = 0; i < 1000; i++){
System.out.println("主线程--->" + i);
}
}
}
class MyThread extends Thread {
@Override
public void run() {
// 编写程序,这段程序运行在分支线程中(分支栈)。
for(int i = 0; i < 1000; i++){
System.out.println("分支线程--->" + i);
}
}
}
注意:
t.run() 不会启动线程,只是普通的调用方法而已。不会分配新的分支栈。(这种方式就是单线程。)
t.start() 方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。
启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。
继承Runnable接口
编写一个类,实现 java.lang.Runnable 接口,实现run方法。
但要创建Thread类,把实现Runnable 接口的类作为参数传入。
// 定义一个可运行的类
public class MyRunnable implements Runnable {
public void run(){
}
}
// 创建线程对象
Thread t = new Thread(new MyRunnable());
// 启动线程
t.start();
eg.
public class ThreadTest03 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
// 启动线程
t.start();
for(int i = 0; i < 100; i++){
System.out.println("主线程--->" + i);
}
}
}
// 这并不是一个线程类,是一个可运行的类。它还不是一个线程。
class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println("分支线程--->" + i);
}
}
}
同步代码块
Java提供了一种内置的锁机制来支持原子性:同步代码块
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁。线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
synchronized (任意对象){
多条语句操作共享数据
}
同步方法
用 synchronized 关键字给针对共享资源进行操作的方法加锁。一个方法使用 synchronized 关键字修饰后,当一个线程A使用这个方法时,其他线程想使用这个方法时就必须等待,直到线程A使用完该方法。
注意:
静态同步方法的锁是给该类的 类对象 加锁。
非静态同步方法其实就是给 this对象 进行加锁。
Lock锁
synchronized被称为隐式锁,会自动获取释放锁,是非公平锁;Lock被称为显示锁,需要显示的手动获取释放锁,可以设置是否为公平锁。
Lock与synchronized一样可以实现线程同步,但比synchronized更强大,粒度更小,更灵活。
lock()获取锁。
unlock()释放锁。
代码(使用lock锁与同步方法):
public class SellTicket implements Runnable {
private int ticket = 100;
private Object object = new Object();
// Lock lock=new ReentrantLock();
@Override
public void run() {
while (true) {
synchronized (object) {
try {
// lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "卖第" + ticket + "张票");
ticket--;
}
} finally {
// lock.unlock();
}
}
// }
}
}
}