介绍多线程之前要介绍线程,介绍线程则离不开进程。
进程可以看做一个程序。
线程是一个程序的执行单元/一个场景。
进程与线程的区别
进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。(每一个线程就是一个栈)
多线程就是一个进程运行时产生了多个线程。

步骤:①、定义类继承Thread;
②、复写Thread类中的run方法;
目的:将自定义代码存储在run方法,让线程运行
③、调用线程的start方法:
该方法有两步:启动线程,调用run方法。
- public class Test3 extends Thread{
- @Override
- public void run() {
- for (int i = 0; i < 10; i++) {
- System.out.println(i);
- }
- }
- }
-
- public class Test4 {
- public static void main(String[] args) {
- Test3 test3 = new Test3();
- test3.start();
- }
- }
注意.run() 不会启动线程,只是普通的调用方法而已。不会分配新的分支栈。(这种方式就是单线程。)
t.start() 方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。
启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。
实现Runnable接口:
实现步骤: ①、定义类实现Runnable接
②、覆盖Runnable接口中的run方法,将线程要运行的代码放在该run方法中。
③、通过Thread类建立线程对象。
④、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程执行指定对象的run方法就要先明确run方法所属对象。
⑤、调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
- public class Test3 implements Runnable{
-
- @Override
- public void run() {
- for (int i = 0; i < 10; i++) {
- System.out.println(i);
- }
- }
- }
-
- public class Test4 {
- public static void main(String[] args) {
- Thread thread = new Thread(new Test3());
- thread.start();
- }
- }
3、通过Callable和Future创建线程:
实现步骤:①、创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。
②、创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值
③、使用FutureTask对象作为Thread对象启动新线程。
④、调用FutureTask对象的get()方法获取子线程执行结束后的返回值。
- public class Test3 implements Callable {
- @Override
- public Integer call() throws Exception {
- int sum=0;
- for (int i = 0; i < 10; i++) {
- sum+=i;
- }
- return sum;
- }
- }
-
-
- public class Test4 {
- public static void main(String[] args) throws ExecutionException, InterruptedException {
- Test3 test3 = new Test3();
- ExecutorService es = Executors.newFixedThreadPool(1);
-
- Future
future = es.submit(test3); -
- Integer i = future.get();
- System.out.println(i);
- }
- }
继承Thread:
优点:编写简单,可直接用this.getname()获取当前线程,不必使用Thread.currentThread()方法。
缺点:已经继承了Thread类,无法再继承其他类。
实现Runnable:
优点:避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
缺点:比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。
实现Callable:
优点:有返回值、避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
缺点:比较复杂、访问线程必须使用Thread.currentThread()方法
在实际开发过程中一般使用实现Runnable的方式创建多线程。
| 方法名 | 作用 |
|---|---|
| static Thread currentThread() | 获取当前线程对象 |
| String getName() | 获取线程对象名字 |
| void setName(String name) | 修改线程对象名字 |
当线程没有设置名字的时候,默认的名字是什么?
| 方法名 | 作用 |
|---|---|
| static void sleep(long millis) | 让当前线程休眠millis秒 |
静态方法:Thread.sleep(1000);
参数是毫秒
作用: 让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。
这行代码出现在A线程中,A线程就会进入休眠。
这行代码出现在B线程中,B线程就会进入休眠。
Thread.sleep()方法,可以做到这种效果:
间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。
| 方法名 | 作用 |
|---|---|
| void interrupt() | 终止线程的睡眠 |
| 方法名 | 作用 |
|---|---|
| int getPriority() | 获得线程优先级 |
| void setPriority(int newPriority) | 设置线程优先级 |
当多个线程访问是就可能会出现线程安全
一个多窗口买票:
- public class MP implements Runnable{
- private static int a=100;
- private Object obj = new Object();
- @Override
- public void run() {
- while (true){
-
- if (a > 0) {
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName() + "票号" + a);
- a--;
-
- }
- }
- }
- }
-
-
- public class Test3 {
- public static void main(String[] args) {
- MP mp = new MP();
- Thread thread1 = new Thread(mp,"一号");
- Thread thread2 = new Thread(mp,"二号");
- Thread thread3 = new Thread(mp,"三号");
- Thread thread4 = new Thread(mp,"四号");
-
- thread1.start();
- thread2.start();
- thread3.start();
- thread4.start();
- }
- }

我们可以看到,这里出现了一些重复票,为什么会出现这种情况,出现这种情况显然表明我们这个方法根本就不是线程安全的,出现这种问题的原因有很多,我们说最常见的一种,就是我们A线程在进入方法后,拿到了a的值,刚把这个值读取出来还没有改变a的值的时候,结果线程B也进来的,那么导致线程A和线程B拿到的a值是一样的。
格式:
-
- synchronized (锁对象) {
-
- 可能会产生线程安全问题的代码
- }
把上述代码加上synchronized后
- public class MP implements Runnable{
- private static int a=100;
- private Object obj = new Object();
- @Override
- public void run() {
- while (true){
- synchronized (obj) {
- if (a > 0) {
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName() + "票号" + a);
- a--;
- }
- }
- }
- }
- }

可以看到没有了重复票
提示:加上同步后只有一个线程在执行是CPU太好了,可以加大基数就可以看到多个线程。
在JDK5之前,锁的获取和释放都是隐式的,看不见。也就说我们并没有直接看到在哪里加上了锁,在哪里释放了锁。到了JDK5的时候,java中提供了一个Lock接口,在这个接口中定义了锁的释放和获取的方法,后期程序中需要使用同步,这时可以使用Lock接口的实现类显示的完成锁的获取和释放的动作。
public void lock():加同步锁。
public void unlock():释放同步锁。
注意:在使用Lock锁对象时,需要手动的书写代码用来:获取锁、释放锁
由于Lock属于接口,不能创建对象,所以我们可以使用它的子类ReentrantLock来创建对象并使用Lock接口中的函数。
用Lock改造上述买票后代码
- public class MP implements Runnable{
- private static int a=100;
- private Object obj = new Object();
- Lock l=new ReentrantLock();
- @Override
- public void run() {
- while (true){
- l.lock();
- if (a > 0) {
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName() + "票号" + a);
- a--;
- }
- l.unlock();
- }
- }
- }
同步的前提:
1、必须要有两个或者两个以上的线程。
2、必须是多个线程使用同一个锁。
3、必须保证同步中只能有一个线程在运行。
4、只能同步方法,不能同步变量和类。
5、不必同步类中所有方法,类可以拥有同步和非同步的方法。
6、如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。
- public class DeadLock {
-
- public static void main(String[] args) {
- Thread t1 = new Thread(new DeadLockTest(true));
- Thread t2 = new Thread(new DeadLockTest(false));
- t1.start();
- t2.start();
- }
- }
-
- class DeadLockTest implements Runnable {
-
- private boolean flag;
- static Object obj1 = new Object();
- static Object obj2 = new Object();
-
- public DeadLockTest(boolean flag) {
- this.flag = flag;
- }
-
- public void run() {
- if (flag) {
- synchronized (obj1) {
- System.out.println("if lock1");
- synchronized (obj2) {
- System.out.println("if lock2");
- }
- }
- } else {
- synchronized (obj2) {
- System.out.println("else lock2");
- synchronized (obj1) {
- System.out.println("else lock1");
- }
- }
- }
- }
- }

注意:在开发中一旦发生了死锁现象,不能通过程序自身解决。必须修改程序的源代码。
在开发中,死锁现象可以避免,但不能直接解决。当程序中有多个线程时,并且多个线程需要通过嵌套对象锁(在一个同步代码块中包含另一个同步代码块)的方式才可以操作代码,此时就容易出现死锁现象。
可以使用一个同步代码块解决的问题,不要使用嵌套的同步代码块,如果要使用嵌套的同步代码块,就要保证同步代码块的上的对象锁使用同一个对象锁(唯一的对象锁)