1.线程和进程

什么是线程和进程
进程就是一个应用程序,线程是一个进程中的执行场景/执行单元。
一个进程可以有多个线程。

对于java来说,在DOC命令下输入java HelloWorld并按下回车时,会启动一个JVM,这就是一个进程,JVM会启动一个主线程调用main方法,还会调用一个回收垃圾的线程,此时java程序中至少有两个线程并发。

进程A和进程B的资源不共享,但同一个进程中的线程C和线程D有共享的资源,也有各自的资源。
1.png

在内存中,不同的线程共用一份方法区和堆内存,而不同的线程会开辟不同的栈空间,事实上在内存中栈内存不仅仅有一份。
假设程序中有10个线程,就会有10个栈内存空间,每个进程之间互不干扰,每个栈内存之间互不干扰,比如main方法结束了,但程序还有可能没有结束,因为main方法结束只是意味着主栈空了,其他的栈还在压栈和弹栈,其他的进程还未结束。这就是并发机制,java中之所以有并发机制,就是为了提高程序的处理效率。

关于CPU
实际上单核CPU的设备并不能做到真正的多线程,我们之所以认为很多应用程序在同时进行,是因为CPU处理的速度极快,多个进程之间频繁切换。


2.创建线程

创建线程的第一种方式:继承Threah类

  1. public class demo {
  2. public static void main(String[] args) {
  3. myThread t1 = new myThread();
  4. // 直接调用run方法,等价于执行一个对象中的普通方法,不会创建一个新的线程
  5. t1.run();
  6. // 调用start方法,作用是创建一个新的栈内存,然后该方法结束
  7. // 调用start方法之后,会在另一个线程中自动调用线程对象的run方法
  8. // 而且run方法会在支栈的底部,就像main方法在主栈的底部一样。
  9. t1.start();
  10. // 注意:一定是执行了start方法之后才会执行main方法中之后的代码、,因为start方法在主栈中
  11. //...
  12. }
  13. }
  14. class myThread extends Thread{
  15. @Override
  16. public void run() {
  17. // 在这里写新的线程需要执行的代码
  18. }
  19. }

创建线程的第二种方法:实现Runnable方法

  1. public class demo {
  2. public static void main(String[] args) {
  3. myThread t = new myThread();
  4. // 注意,线程的创建一定是由Thread类实例化产生的
  5. Thread t1 = new Thread(t);
  6. }
  7. }
  8. // 这还不是一个线程类,实例化它的对象也不能算是创建一个线程
  9. class myThread implements Runnable{
  10. @Override
  11. public void run() {
  12. // 在这里写新的线程需要执行的代码
  13. }
  14. }

同时我们可以使用匿名内部类的方式用第二种方式创建线程

  1. Thread t1 = new Thread(new Runnable() {
  2. @Override
  3. public void run() {
  4. //...
  5. }
  6. });

要注意:我们更应该使用第二种方法创建线程,因为我们定义的实现接口的类可能还需要继承其他的类。


3.线程的五个状态

1.新建状态
2.死亡状态
3.就绪状态
4.运行状态
5.阻塞状态

2.png


4.线程中的几个方法

1.setName方法,修改线程的名字。
2.getName方法,获取线程的名字。
3.currentThread方法,获取当前的线程,这是一个静态的方法,返回值是Thread类型的。

  1. public class demo {
  2. public static void main(String[] args) {
  3. myThread t = new myThread();
  4. // setName方法
  5. t.setName("线程x");
  6. t.start();
  7. }
  8. }
  9. class myThread extends Thread{
  10. @Override
  11. public void run() {
  12. for(int i=0 ; i<100 ; i++){
  13. // currentThread方法和getName方法
  14. System.out.println(Thread.currentThread().getName()+"--->"+i);
  15. }
  16. }
  17. }

currentThread方法的一个说明

  1. public class demo {
  2. public static void main(String[] args) {
  3. // 多态
  4. Thread t = new myThread();
  5. // 看看用对象名调用currentThread是什么结果
  6. String s = t.currentThread().getName();
  7. // 输出main
  8. // 意味着currentThread方法和对象没有关系
  9. System.out.println(s);
  10. }
  11. }
  12. class myThread extends Thread{
  13. @Override
  14. public void run() {
  15. for(int i=0 ; i<100 ; i++){
  16. // currentThread方法和getName方法
  17. System.out.println(Thread.currentThread().getName()+"--->"+i);
  18. }
  19. }
  20. }

4.sleep方法,用于让正在进行的线程进入阻塞状态,放弃占有的cpu时间片,让给其他线程使用。这是一个静态方法,参数是毫秒数。

  1. public class demo {
  2. public static void main(String[] args) {
  3. Thread t = new myThread();
  4. t.start();
  5. }
  6. }
  7. class myThread extends Thread{
  8. @Override
  9. public void run() {
  10. for(int i=0 ; i<5 ; i++){
  11. // currentThread方法和getName方法
  12. System.out.println(Thread.currentThread().getName()+"--->"+i);
  13. // 注意sleep要处理异常,在而且只能用try catch,不能throws
  14. // 因为Thread类没有抛出异常,子类不能比父类有更多更广的异常
  15. try {
  16. // 睡眠1秒
  17. Thread.sleep(1000);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. }
  23. }

注意,在main函数中如果写sleep线程,就算使用对象调用的形式,也不能让支线程休眠,只能让main函数休眠,因为sleep是静态方法。

5.interrupt方法,这个方法用于中断一个线程的睡眠,这是一个非静态方法。可以main函数里中断另一个线程的休眠。
注意:interrupt方法不是让线程进入运行状态,而是让线程进入就绪状态,被JVM调配。

  1. public class demo {
  2. public static void main(String[] args) {
  3. Thread t = new myThread();
  4. t.start();
  5. //...
  6. // 希望在前面的代码结束之后让t线程醒过来,把cpu时间片让给t
  7. t.interrupt();
  8. // 这种中断睡眠的方法依赖了java的异常处理机制
  9. // 实际上会执行catch中的代码
  10. }
  11. }
  12. class myThread extends Thread{
  13. @Override
  14. public void run() {
  15. for(int i=0 ; i<5 ; i++){
  16. try {
  17. // 睡眠1秒
  18. System.out.println("t线程正在执行");
  19. Thread.sleep(1000);
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. }
  25. }

中断线程的一种方法:用一个属性就行了

  1. public class demo {
  2. public static void main(String[] args) {
  3. myThread t = new myThread();
  4. t.start();
  5. try {
  6. Thread.sleep(2000);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. // 我想终止线程只需要把对象中的flat改成flase就可以了
  11. t.flat = false;
  12. }
  13. }
  14. class myThread extends Thread {
  15. // 在我的类中定义一个boolean类型的属性
  16. boolean flat = true;
  17. @Override
  18. public void run() {
  19. for (int i = 0; i < 10; i++) {
  20. // 注意这个程序中判断要在for里,把判断放在for外没有意义,程序照常执行。
  21. // 如果flat为真时,便让run方法继续执行
  22. if (flat) {
  23. try {
  24. // 睡眠1秒
  25. System.out.println("t线程正在执行");
  26. Thread.sleep(1000);
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. } else {
  31. System.out.println("线程终止!");
  32. return;
  33. }
  34. }
  35. }
  36. }

5.关于线程调度问题

在开发中有两种线程调度模型
1.抢占式调度模型:哪个线程的有限度高,抢到的CPU时间片的概率就高一些/多一些,java采用的就是抢占式调度模型。
2.均分式调度模型:平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样,平均分配,一切平等,有一些编程语言的线程调度模型采用的就是这种方法。

java中提供了关于线程调度的一些方法

void setPriority (int newPriority),设置线程的优先级
int getPriority(),获取线程的优先级
最低优先级1,默认优先级5,最高优先级10
优先级比较高的获取CPU时间篇可能会多一些,但不完全是。
statuc void yield(),一个静态方法,让位方法,暂停当前正在执行的线程对象,并执行其他线程,yield方法的执行会让当前线程从“运行状态”回到“就绪状态”。


6.线程安全

前程安全是并发开发中的重点,因为我们的项目都运行在服务器中,关于线程对象的创建,线程的启动等,都已经实现完了。
重要的是,我们编写的程序要放在一个多线程的环境下,我们要关注的是一些数据在多线程并发的环境下是否是安全的。
当有多线程并发的环境,有共享数据的存在,而且共享数据有修改的行为。

怎样解决线程安全问题
使用线程同步机制,实际上就是让线程排队执行,不并发,就可以解决线程安全问题了。
线程排队会牺牲一部分效率,但此时会保证线程是安全的,数据的安全永远是前提。

异步编程机制:t1线程和t2线程各自执行各自的,实际上就是多线程并发。
同步变成机制:两个线程之间发生了等待关系,这就是同步编程模型。

重要案例一:银行取钱模型

  1. // Account类
  2. package Account;
  3. public class Account {
  4. private String ac;
  5. private double money;
  6. public Account(){
  7. }
  8. public Account (String ac,double money){
  9. this.ac = ac;
  10. this.money = money;
  11. }
  12. public String getAc() {
  13. return ac;
  14. }
  15. public double getMoney() {
  16. return money;
  17. }
  18. public void setAc(String ac) {
  19. this.ac = ac;
  20. }
  21. public void setMoney(double money) {
  22. this.money = money;
  23. }
  24. //取钱
  25. public void withdraw(double money){
  26. // 当前账户有多少前
  27. double befor = this.money;
  28. // 取之后剩余多少前
  29. double after = befor - money;
  30. try {
  31. // 模拟网络延时,一定会出问题
  32. Thread.sleep(1000);
  33. } catch (InterruptedException e) {
  34. e.printStackTrace();
  35. }
  36. // 修改setMoney
  37. this.setMoney(after);
  38. }
  39. }
  1. // myThread类
  2. package Account;
  3. public class myThread extends Thread{
  4. private Account acc;
  5. public myThread(Account acc){
  6. this.acc = acc;
  7. }
  8. @Override
  9. public void run() {
  10. acc.withdraw(2500);
  11. // 打印信息
  12. System.out.println(Thread.currentThread().getName()+"取款2500,剩余"+acc.getMoney());
  13. }
  14. }
  1. // 测试类
  2. package Account;
  3. public class test {
  4. public static void main(String[] args) {
  5. Account acc = new Account("账户1",5000);
  6. myThread t1 = new myThread(acc);
  7. myThread t2 = new myThread(acc);
  8. t1.setName("t1");
  9. t2.setName("t2");
  10. t1.start();
  11. t2.start();
  12. }
  13. }

输出结果
t1取款2500,剩余2500.0
t2取款2500,剩余2500.0

java中使用锁机制保证了线程安全问题

  1. public void withdraw(double money){
  2. // 加上synchronized关键字
  3. synchronized (this){
  4. // 当前账户有多少前
  5. double befor = this.money;
  6. // 取之后剩余多少前
  7. double after = befor - money;
  8. try {
  9. Thread.sleep(1000);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. // 修改setMoney
  14. this.setMoney(after);
  15. }
  16. }

synchronized后面小括号中传入的“数据”是相当关键,这个数据必须是多线程共享的数据,才能达到多线程排队的效果。
在我们的程序中括号里是this对象,而这个this对象是主类中的Account类的实例对象,只有一个,虽然我们创建了两个线程,当这两个线程共用一个Account对象,而一个Account对象只有一把锁,t1/t2占用了这把锁,就会让之后的线程排队。

实际上不一定必须要是this
可以是字符串”abc”,因为字符串在常量池中只有一个,但缺陷就是如果再实例化一个Account对象,那么两个对象就只有一把锁了。

  1. Object obj = new Object(); // 如果在类中实例化一个对象,把这个对象放在小括号里也是可以的
  2. // 因为内存中的堆只有一份,就算实例化了两个Account对象,他们只会指向堆中的一个obj地址,公用的。

关于锁池
3.png

关于变量的安全问题

实例变量:在堆中,堆在内存中只有一个,不同的线程操作堆时,就是并发操作,会存在安全问题。
静态变量:在方法区中,方法区中的静态变量也只有一个,和实例变量一样会存在安全问题。
局部变量:在栈中,永远都不会存在线程安全问题,因为局部变量不共享,一个线程一个栈,局部变量在栈中,所以局部变量永远都不会被共享。

  1. public void withdraw(double money){
  2. int i = 100;
  3. i = 101;}

t1会修改t1栈中的i,t2会修改t2栈中的i,不存在线程安全问题。

在方法上也可以使用synchronized

  1. public synchronized void withdraw(double money)

优点:代码简洁
缺点:此时锁的一定是this对象,不能是其他对象,这种方式不灵活。

举个例子

关于StringBuffer类和StringBuilder类,如果我们有一个局部变量中要用到StringBuffer类,不妨使用StringBuilder类,因为StringBuilder类,因为StringBuilder类虽然存在线程不安全问题,但是我们操作的局部变量一定不存在线程安全问题。

注:synchronized出现在静态方法上是找类锁。

死锁现象

死锁现象是由于对synchronized的使用不当,造成程序的死锁,这种错误不会出现异常,也不会出现错误,程序会一直僵持在那里,这种错误最难调试。

  1. public class demo1 {
  2. public static void main(String[] args) {
  3. Object o1 = new Object();
  4. Object o2 = new Object();
  5. // t1,t2共用o1和o2对象
  6. myThread1 t1 = new myThread1(o1,o2);
  7. myThread1 t2 = new myThread1(o1,o2);
  8. t1.start();
  9. t2.start();
  10. }
  11. }
  12. class myThread1 extends Thread{
  13. Object o1;
  14. Object o2;
  15. public myThread1 (Object o1,Object o2){
  16. this.o1 = o1;
  17. this.o2 = o2;
  18. }
  19. @Override
  20. public void run() {
  21. synchronized (o1){
  22. try {
  23. sleep(1000);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. synchronized (o2){
  28. }
  29. }
  30. }
  31. }
  32. class myThread2 extends Thread{
  33. Object o1;
  34. Object o2;
  35. public myThread2 (Object o1,Object o2){
  36. this.o1 = o1;
  37. this.o2 = o2;
  38. }
  39. @Override
  40. public void run() {
  41. synchronized (o2){
  42. try {
  43. sleep(1000);
  44. } catch (InterruptedException e) {
  45. e.printStackTrace();
  46. }
  47. synchronized (o1){
  48. }
  49. }
  50. }
  51. }

涉及到了synchronized的嵌套就可能导致死锁现象,在开发中最好不要使用锁的嵌套使用。

在实际的开发中应该怎样解决线程安全问题

实际上一上来就使用synchronized选择线程的同步不是一个最好的选择,synchronized会使程序的执行效率变低,用户体验差,在不得已的情况下在选择线程同步机制。

第一种方案,尽量使用局部变量代替成员变量,在多线程开发中成员变量都在堆中,而堆只有一个,一定会涉及到数据的共享,使用局部变量,一个局部变量一个栈,使得数据不共享,不存在线程不安全问题,比如说在run方法中定义变量。

第二种方案,如果要使用实例变量/成员变量,可以考虑创建多个对象,不同对象的实例变量的内存不共享。比如说创造多个Account对象。

第三种方案,只能选择synchronized,使用线程同步机制。

7.守护线程


我们以上定义的线程都叫用户线程,实际上java语言中还有一类线程是守护线程,也叫做后台线程,具有代表性的线程就是垃圾回收线程,当用户线程/主线程(主线程main方法就是一个用户线程)结束时,守护线程自动结束。一般来说守护线程是一个死循环。

  1. public class demo1 {
  2. public static void main(String[] args) {
  3. myThread t1 = new myThread();
  4. t1.setName("守护线程");
  5. // 我希望我的主线程的for循环结束之后,守护线程也结束,用到setDaemon方法
  6. t1.setDaemon(true);
  7. t1.start();
  8. for (int i=1 ; i<10 ; i++){
  9. try {
  10. Thread.sleep(1000);
  11. System.out.println(Thread.currentThread().getName()+"--->"+i);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. }
  17. }
  18. class myThread extends Thread{
  19. @Override
  20. public void run() {
  21. int i = 0;
  22. // 守护线程是个死循环
  23. while (true){
  24. try {
  25. Thread.sleep(1000);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. i++;
  30. System.out.println(Thread.currentThread().getName()+"--->"+i);
  31. }
  32. }
  33. }

8.生产者消费者模型


首先引入Object类中的两个有关线程的方法,wait方法和notify方法。这两个方法是java中任何一个对象都有的方法,他们不是通过线程调用的,而是通过普通方法调用的。

wait方法,表示让正在对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。会释放掉该线程之前占有的对象的锁。
notify方法,唤醒正在对象上等待的线程,还有一个notifyAll方法,唤醒在对象上处于等待的所有线程。只是通知,不会释放对象上之前占有的锁。

关于生产者消费者模型
4.png

生产者消费者模型模拟了一个这样的需求,
我们用list集合,假设在集合中只能存储1个元素,有1个元素就表示仓库满了,0个元素表示仓库空了,必须要做到这样的一个效果:生产1个消费1个。

  1. // 主类
  2. package ProducerandConsumer;
  3. import java.util.LinkedList;
  4. import java.util.List;
  5. public class test {
  6. public static void main(String[] args) {
  7. // list模拟了一个仓库
  8. List list = new LinkedList();
  9. // 模拟了两个线程
  10. Producer pro = new Producer(list);
  11. Consumer con = new Consumer(list);
  12. pro.setName("生产");
  13. con.setName("消费");
  14. pro.start();
  15. con.start();
  16. }
  17. }
  1. // 模拟了一个消费者类
  2. package ProducerandConsumer;
  3. import java.util.List;
  4. public class Consumer extends Thread {
  5. List list;
  6. public Consumer(List list) {
  7. this.list = list;
  8. }
  9. @Override
  10. public void run() {
  11. // 让消费者一直消费
  12. while (true) {
  13. // 当消费的时候锁住list
  14. synchronized (list) {
  15. if (list.size() == 0) {
  16. // 不能消费,等待生产线程
  17. // 此时释放了锁等生产者生产
  18. // 消费者线程停止
  19. try {
  20. list.wait();
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. // 程序执行到这里,说明可以消费,开始消费
  26. Object o = list.remove(0);
  27. System.out.println(Thread.currentThread().getName() + "--->" + o);
  28. // 通知生产者可以生产了,唤醒了list对象上的线程
  29. list.notify();
  30. }
  31. }
  32. }
  33. }
  1. // 生产者类
  2. package ProducerandConsumer;
  3. import java.util.List;
  4. public class Producer extends Thread {
  5. List list;
  6. public Producer(List list) {
  7. this.list = list;
  8. }
  9. @Override
  10. public void run() {
  11. while (true) {
  12. synchronized (list) {
  13. if (list.size() > 0) {
  14. // 不能生产,等待消费
  15. try {
  16. list.wait();
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. }
  21. // 开始生产
  22. Object o = new Object();
  23. list.add(o);
  24. System.out.println(Thread.currentThread().getName() + "--->" + o);
  25. list.notify();
  26. }
  27. }
  28. }
  29. }