程序、进程、线程
程序:是为了完成特定任务,用某种语言编写的一组指令的集合,是一段静态的代码。(程序是静态的)
进程:是程序的一次动态执行。正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。(进程是动态的),进程的生命周期:有它自身的产生、存在和消亡的过程。
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。若一个进程同一时间并行执行多个线程,就是支持多线程的。
单核CPU与多核CPU
单核CPU:CPU在执行的时候,是按照时间片执行的,同一个时间片只能执行一个任务。执行多个任务时,会在多个任务之间来回跳。时间片切换很快,给人感觉像是在同时执行多个线程。是一种假象。
多核CPU:多个CPU的时候,才真正意义上做到一个时间片多线程同时执行。多线程发挥了最好的效率。真正的多线程。
并行和并发
并行:多个CPU同时执行多个任务。
并发:一个CPU“同时”执行多个任务。(采用时间片切换)
在学习多线程之前,以前的代码都是单线程吗?
不是,以前的代码也是多线程。除了main方法为主线程,还包括处理异常的线程和垃圾回收的线程。PS:处理异常的线程会影响主线程的执行。
创建线程的三种方式
1. 继承Thread类
public class BuyTicketThread extends Thread{
public BuyTicketThread(String name){
super(name);
}
//一共10张票
static int ticketNum = 10;//加上static,使得票数被三个线程共享,否则会售出30张票
@Override
public void run(){
//每个窗口后面有100个人在抢票
for (int i = 1; i <= 100; i++) {
//票数大于0才售票
if (ticketNum>0){
System.out.println("我在"+this.getName()+"抢到了从北京到邯郸的第"+ ticketNum-- +"张票");
}
}
}
}
public class Test {
public static void main(String[] args) {
//多个窗口抢票
BuyTicketThread buy1 = new BuyTicketThread("窗口1");
buy1.start();
BuyTicketThread buy2 = new BuyTicketThread("窗口2");
buy2.start();
BuyTicketThread buy3 = new BuyTicketThread("窗口3");
buy3.start();
}
}
2.实现Runnable接口
public class BuyTicketThread implements Runnable{
int ticketNum = 10;
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
if (ticketNum > 0){
System.out.println("我在"+Thread.currentThread().getName()+"买到了第"+ ticketNum-- +"张北京到邯郸的车票");
}
}
}
}
public class Test {
public static void main(String[] args) {
//定义一个线程对象
BuyTicketThread buy = new BuyTicketThread();
//窗口1买票
Thread thread1 = new Thread(buy,"窗口1");
thread1.start();
//窗口2买票
Thread thread2 = new Thread(buy,"窗口2");
thread2.start();
//窗口3买票
Thread thread3 = new Thread(buy,"窗口3");
thread3.start();
}
}
3.实现Callable接口
对比方式1和方式2,都需要有一个run方法,但是这个run方法有不足之处:没有返回值,不能抛出异常。
基于上面两个不足,在JDK1.5之后出现了第三种创建多线程的方式:实现Callable接口:有返回值,可以抛出异常,但是创建比较麻烦。
public class TestCallable implements Callable<Integer> {
/**
* 实现Callable接口可以不带泛型,如果不带则返回值是object类型
* @return
* @throws Exception
*/
@Override
public Integer call() throws Exception {
return new Random().nextInt(10);//返回10以内的随机数
}
}
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//定义一个线程对象
TestCallable tc = new TestCallable();
FutureTask ft = new FutureTask(tc);
Thread thread = new Thread(ft);
thread.start();
//获取线程的返回值
Object obj = ft.get();
System.out.println(obj);
}
}
方式1继承Thread类和方式2实现Runnable接口,实际开发中哪个方式更好?
方式1在Java单继承中具有局限性,继承了Thread类,就不能继承其他类了。
方式2的共享资源能力更强一些,变量不用加static来修饰也可以实现共享,方式1共享变量必须用static修饰。(为什么不用static也可以) https://blog.csdn.net/zuolovefu/article/details/40264719
Thread类和Runnable接口有什么关系?
Runnable接口--实现-->Thread类,即Thread类实现了Runnable接口。(方式2的线程类实现了Runnable接口,方式1的线程类继承了Thread类。)
线程的生命周期
新生状态、就绪状态、执行状态、死亡状态、阻塞状态。
Thread thread = new Thread();新生状态-----start启动----》就绪状态(等待CPU调度)----run()获取cpu执行权--------》运行状态-------正常结束/出现异常/调用stop方法(不建议,该方法已废弃)----------》死亡状态
Thread thread = new Thread();新生状态-----start启动----》就绪状态(等待CPU调度)----run()获取cpu执行权--------》运行状态-------阻塞事件----------》阻塞状态-------阻塞事件解除----------------》就绪状态
Thread thread = new Thread();新生状态-----start启动----》就绪状态(等待CPU调度)----run()获取cpu执行权--------》运行状态------调用wait方法----》等待队列-----调用notify/notifyall方法-------》锁池状态-------释放锁-》就绪状态
Thread thread = new Thread();新生状态-----start启动----》就绪状态(等待CPU调度)----run()获取cpu执行权--------》运行状态------synchronized----》锁池状态-----释放锁-----》就绪状态
线程的常见方法
start():启动当前线程,表面上调用start方法,实际上执行run方法。
run():线程类继承Thread或者实现runnable接口的时候,都要重写run方法,run方法里面是线程要执行的内容。
currentThread:Thread类中的一个静态方法,获取正在执行的线程。
setName():设置线程名称
getName():获取线程名称
setPriority(int num):设置优先级别方法:同优先级别的线程采取先到先服务,使用时间片策略。如果优先级高,被CPU调度的概率就高。级别为:1-10,默认是5。
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
设置优先级别
public class TestThread01 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
public class TestThread02 extends Thread{
@Override
public void run() {
for (int i = 20; i < 30; i++) {
System.out.println(i);
}
}
}
public class Test {
public static void main(String[] args) {
TestThread01 thread01 = new TestThread01();
thread01.setPriority(10);//优先级别高
thread01.start();
TestThread02 thread02 = new TestThread02();
thread02.setPriority(2);//优先级别低
thread02.start();
}
}
join方法
当一个线程调用了join方法,这个线程就会先被执行,它执行结束以后才可以去执行其余的线程。注意:必须先start,再join才有效。
public class TestThreadJoin extends Thread{
public TestThreadJoin(String name){
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.getName()+"----------"+i);
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
System.out.println("main-------------"+i);
if (i==6){
//创建子线程
TestThreadJoin ttj = new TestThreadJoin("join子线程");
ttj.start();
ttj.join();//等子线程执行完再执行主线程。子线程打印0-9
}
}
}
}
sleep方法
sleep人为制造阻塞事件
public class Test {
public static void main(String[] args) throws InterruptedException {
DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
Date date = new Date();
String format = dateFormat.format(date);
System.out.println(format);
Thread.sleep(3000);
}
}
设置伴随线程
将子线程设置为主线程的伴随线程,主线程停止的时候,子线程也不要继续执行了。不会立马结束,会垂死挣扎一会。
public class TestThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("子线程--"+i);
}
}
}
class Test{
public static void main(String[] args) {
//创建并启动子线程
TestThread thread = new TestThread();
thread.setDaemon(true);//设置伴随线程,先设置,再启动
thread.start();
//主线程输出1到10
for (int i = 0; i < 10; i++) {
System.out.println("main---"+i);
}
}
}
stop方法
//终止当前线程。
public class TestStop{
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
if (i==6){
Thread.currentThread().stop();//过期方法,不建议使用
}
System.out.println(i);
}
}
}
同步代码块
买票代码出现的问题:
1.出现了两个或多个10张票。因为ticketNum--时,执行了两个操作,一个是打印ticketNum,一个是ticketNum减1,很可能在ticketNum没有执行减1操作时,被其他线程抢到了资源先打印出来,导致多售票的现象发生。
2.出现0或者-1,-2的情况。跟上述一样的情况,先执行了减1操作,后执行了打印。
上面代码出现的问题:出现了重票,多票。
原因:多个线程,在争抢资源中,导致共享的资源出现了问题。一个线程没执行完,另一个线程参与进来,开始争抢。
解决:在程序中加入锁。
方式1:同步代码块 this锁
public class TestSyncThread implements Runnable{
int ticketNum = 10;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
synchronized (this) {//只把具有安全隐患的代码锁住即可,如果锁多了效率就会降低。-》this指的就是这个锁
if (ticketNum>0){
System.out.println("我在"+Thread.currentThread().getName()+"买到了邯郸到北京的第"+ ticketNum-- +"张票");
}
}
}
}
}
public class Test {
public static void main(String[] args) {
TestSyncThread tst = new TestSyncThread();
Thread thread = new Thread(tst,"窗口1");
thread.start();
Thread thread2 = new Thread(tst,"窗口2");
thread2.start();
Thread thread3 = new Thread(tst,"窗口3");
thread3.start();
}
}
同步代码块 字节码信息锁
public class TestSyncLockThread extends Thread{
static int ticketNum =10;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
synchronized (TestSyncLockThread.class) {
//必须是同一把锁,因为创建了三个Thread对象,如果这里用this,相当于用了三把锁。为了保证同一把锁,这里一般写类的字节码表示唯一。
if (ticketNum>0){
System.out.println("我在"+Thread.currentThread().getName()+"抢到了从北京到邯郸的第"+ ticketNum-- +"张票");
}
}
}
}
}
public class Test {
public static void main(String[] args) {
Thread thread1 = new TestSyncLockThread();
thread1.setName("窗口1");
thread1.start();
Thread thread2 = new TestSyncLockThread();
thread2.setName("窗口2");
thread2.start();
Thread thread3 = new TestSyncLockThread();
thread3.setName("窗口3");
thread3.start();
}
}
同步监视器总结:synchronized(同步监视器){},是写在括号中的叫做同步监视器。
1.必须是引用类型,不能是基本数据类型。
2.也可以创建一个专门的同步监视器,没有任何业务含义。Object o = new Object();
3.一般使用共享资源做同步监视器即可。如上面类的字节码。
4.在同步代码块中不能改变同步监视器的引用。错误示例:String syncStr = "abc";synchronized(syncStr){syncStr ="bcd";}
5.尽量不要用String和包装类Integer做同步监视器。
6.建议使用final修饰同步监视器。 final String syncStr = "abc";
同步代码块的执行过程:
1.第一个线程来到同步代码块,发现锁处于open状态,先进行close,然后执行代码块中的代码。
2.第一个线程在执行代码过程中出现了线程切换,(阻塞 就绪),失去了CPU,但是没有开锁open。
3.第二个线程来到同步代码块,发现锁处于close,无法执行代码块中的代码,第二个线程进入阻塞。
4.第一个线程重新拿到CPU,执行完代码,释放锁open。
5.第二个线程也再次拿到CPU,来到同步代码块,发现锁处于open状态,拿到锁并上锁,由阻塞状态变为就绪状态再变为执行状态,重复线程一的过程。
强调:同步代码块中能发生CPU的切换吗?能,但是后续的被执行的线程无法执行代码块中的代码,因为锁仍旧close。
总结:其他
1.多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块。
2.多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块。
同步方法
public class TestSyncMethod implements Runnable{
int ticketNum = 10;
@Override
public void run() {
for (int i = 1; i < 100; i++) {
buyTicket();
}
}
public synchronized void buyTicket(){//对这个方法加同步方法,锁住的是TestSyncMethod.class
if (ticketNum>0){
System.out.println("我在"+Thread.currentThread().getName()+"抢到了第"+ ticketNum-- +"张票");
}
}
}
public class Test {
public static void main(String[] args) {
TestSyncMethod method = new TestSyncMethod();
Thread thread1 = new Thread(method,"窗口1");
thread1.start();
Thread thread2 = new Thread(method,"窗口2");
thread2.start();
Thread thread3 = new Thread(method,"窗口3");
thread3.start();
}
}
public class TestSyncMethod2 extends Thread{
static int ticketNum = 10;
@Override
public void run() {
for (int i = 1; i < 100; i++) {
buyTicket();
}
}
public static synchronized void buyTicket(){//用static修饰,锁住的是TestSyncMethod2.class
if (ticketNum>0){
System.out.println("我在"+Thread.currentThread().getName()+"抢到了第"+ ticketNum-- +"张票");
}
}
}
public class Test2 {
public static void main(String[] args) {
TestSyncMethod2 method1 = new TestSyncMethod2();
method1.setName("窗口1");
method1.start();
TestSyncMethod2 method2 = new TestSyncMethod2();
method2.setName("窗口2");
method2.start();
TestSyncMethod2 method3 = new TestSyncMethod2();
method3.setName("窗口3");
method3.start();
}
}
总结:关于同步方法
1.不要将run()定义为同步方法。
2.非静态同步方法的同步监视器是this。静态同步方法的同步监视器是类的字节码对象。类.class
3.同步代码块的效率要高于同步方法。
原因:同步方法是将线程挡在了方法的外部,而同步代码块可以进入方法,只是在代码块的外部,但却是在方法的内部。
4.同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法。同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他同步监视器的代码块。
lock锁
JDK1.5后新增新一代的线程同步方式线程锁,与采用synchronized相比,lock可提供多种锁方案,更灵活。
synchronized是Java中的关键字,属于虚拟机级别的锁。这个关键字是靠JVM来识别完成的。
Lock锁是API级别的,提供了相应的接口和实现类,这个方式更加灵活,表现出来的性能优于之前的方式。
###### 代码演示:
public class Test {
public static void main(String[] args) {
TestLock testLock = new TestLock();
Thread thread = new Thread(testLock,"窗口1");
thread.start();
Thread thread2 = new Thread(testLock,"窗口2");
thread2.start();
Thread thread3 = new Thread(testLock,"窗口3");
thread3.start();
}
}
public class TestLock implements Runnable{
int ticketNum = 10;
//拿来一把锁
Lock lock = new ReentrantLock();
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
//打开锁
lock.lock();
try {
if (ticketNum>0){
System.out.println("我在"+Thread.currentThread().getName()+"抢到了第"+ ticketNum-- +"张票");
}
} catch (Exception e) {
e.printStackTrace();
} finally {//将关闭锁放到finally中,即使代码异常也会关闭锁
lock.unlock();
}
}
}
}
lock锁和synchronized的区别:
1.lock是显示锁(手动开启和关闭锁,别忘了关闭锁),synchronized是隐式锁。
2.lock只有代码块锁,synchronized有代码块锁和方法锁。
3.使用lock锁,JVM花费较少的时间来调度线程,性能更好。并且具有更好的扩展性。(提供更多的子类)
使用的顺序:
Lock--同步代码块(已经进入了方法体,分配了相应的资源)--同步方法(在方法体之外)
线程同步的优缺点
1.对比:线程安全,效率低。线程不安全,效率高。
2.可能造成死锁:
死锁:
>不同的线程分别占用对方需要的同步资源不放弃,都在等待对方释放自己需要的同步资源,就形成了线程的死锁。
>出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
3.代码演示:
public class Test {
public static void main(String[] args) {
//实现两个线程类
TestDeadLock testDeadLock1 = new TestDeadLock();
TestDeadLock testDeadLock2 = new TestDeadLock();
testDeadLock1.flag=1;
testDeadLock2.flag=0;
//开启两个线程
Thread thread1 = new Thread(testDeadLock1);
thread1.start();
Thread thread2 = new Thread(testDeadLock2);
thread2.start();
}
}
public class TestDeadLock implements Runnable{
public int flag = 1;
//两个对象必须用static修饰,因为必须确定是唯一的元素
static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
System.out.println("flag=="+flag);
if (flag == 1){
synchronized (o1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){//只需要锁住o2即可
System.out.println("o2");
}
}
}
if (flag==0){
synchronized (o2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){//只需要锁住o1即可
System.out.println("o1");
}
}
}
}
}
4.解决办法:减少同步资源的定义,避免嵌套同步。
线程通信问题
在Java对象中,有两种池
锁池-----------------synchronized
等待池---------------wait(),notify(),notifyall()
如果一个线程调用了某个对象的wait方法,那么该线程进入到该对象的等待池中(并且已经将锁释放),
如果未来某个时刻,另外一个线程调用了相同对象的notify方法或者notifyall方法,
那么该等待池中的线程就会被唤醒,然后进入到该对象的锁池里面获取该对象的锁,
如果获得锁成功后,那么该线程就会沿着wait方法之后的路径继续执行。注意是沿着wait()方法之后。
注意:
1. wait方法和notify方法 notifyall方法,必须放在同步代码块或者同步方法里才生效。(因为在同步的基础上进行线程的通信才是有效的)
2. sleep和wait的区别,sleep进入阻塞状态,没有释放锁;wait进入阻塞状态,释放了锁。
使用同步代码块,完成生产消费者通信代码存在的问题:
生产者和消费者在同一个等待池里,如果同时存在多个消费者,则无法实现。解决思路,所有生产者放入一个等待池,所有消费者放入一个等待池。
使用lock锁来实现线程通信:
lock拥有一个同步池和多个等待池,synchronized拥有一个同步池和一个等待池。