public class ThreadTest {
public static void main(String[] args) {
//创建两个线程并启动
//start方法的两个作用:1.启动该线程,2.调用run方法,所以启动一个线程是调用start方法,调用run方法不会启动线程。
new MyThread1().start();
new MyThread1().start();
}
}
class MyThread1 extends Thread{
//输出100以内的偶数
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
执行结果(部分截图):
2.实现Runnable接口创建多线程//实现Runnable接口创建多线程
public class RunnableTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread).start();
new Thread(myThread).start();
}
}
class MyThread implements Runnable{
//输出100以内的偶数
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
执行结果(部分截图):
Thread类部分源码:
public class Thread implements Runnable {
//这里的target通过构造方法初始化
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
实现runnable方法说明:通过构造方法将Runnable接口的实现类赋值给target,调用start方法会调用Thread类中的run方法,判断target是否为空,不为空,则调用target的run方法。
3.两种方式的对比:优先选择实现runnable接口的方式,原因:
1.继承的方式受到类的单继承的限制,继承了Thread类就不能有其他父类,实现接口的方式则不会有这个问题。
2.实现的方式更适合来处理多个线程共享数据的情况。代码举例:
需求:创建两个线程对应两个买票窗口共同来卖100张票
继承方式代码:
public class ThreadTest2 {
public static void main(String[] args) {
new MyThread2("窗口1").start();
new MyThread2("窗口2").start();
}
}
class MyThread2 extends Thread{
public MyThread2(String name){
super(name);
}
//票数模拟买票
//private int ticket=100;
//这里必须使用static,否则ticket将会是一个线程有一份。而不是共享数据
private static int ticket=100;
@Override
public void run() {
while(true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"出售第"+ticket+"张票");
ticket--;
}else{
break;
}
}
}
}
实现runnable方式代码:
package demo01;
public class RunnableTest2 {
public static void main(String[] args) {
MyThread3 myThread3 = new MyThread3();
//两个线程共用一个MyThread3对象
new Thread(myThread3).start();
new Thread(myThread3).start();
}
}
class MyThread3 implements Runnable{
//这里不需要加static
private int ticket=100;
@Override
public void run() {
while(true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"出售第"+ticket+"张票");
ticket--;
}else{
break;
}
}
}
}
所以实现的方式更适合多个线程共享数据的情况。
4.Thread类的常用方法 5.线程的优先级 6.守护线程守护线程依赖于用户线程(默认就是用户线程),当jvm中只剩下守护线程时,当前jvm将退出,jvm中的垃圾回收就是一个典型的守护线程。通过setDaemon(true)可以把一个用户线程设置为守护线程(在start之间)。
代码:
package demo01;
public class DaemonTest {
public static void main(String[] args) {
MyThread4 myThread4 = new MyThread4();
Thread t1 = new Thread(myThread4,"线程1");
Thread t2 = new Thread(myThread4,"线程2");
//将t2设置为守护线程
t1.start();
t2.setDaemon(true);
t2.start();
}
}
class MyThread4 implements Runnable{
//打印1-100
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
执行结果:线程1结束后,线程2并不是马上结束,而是执行了一段时间才结束。
7.线程的生命周期 8.线程的安全问题当多个线程操作同一个共享数据时就会出现线程的安全问题,上面的买票案例中已经存在安全问题了,因为多个窗口共享了100张票。
下面在通过一个案例理解线程的安全问题:
package demo02;
public class AccountTest {
public static void main(String[] args) {
Account account=new Account();
new Thread(()->{
for (int i=0;i<10;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.getMoney(100);
}
}).start();
new Thread(()->{
for (int i=0;i<10;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setMoney(100);
}
}).start();
}
}
class Account{
//账户余额
private int money=0;
//取钱
public void getMoney(int num){
if(money>=num){
money-=num;
System.out.println(Thread.currentThread().getName()+"取了"+num+",余额为:"+money);
}
}
//存钱
public void setMoney(int num){
money+=num;
System.out.println(Thread.currentThread().getName()+"存了"+num+",余额为:"+money);
}
}
执行结果:
分析:两个线程分别对同一个账户存钱和取钱,当他们交替进行时, 就会出现问题,比如:一个线程正在取钱100,余额减少后,被另一个线程抢去了执行权,并执行了存钱操作,当再次回到该线程时,取钱后会发现余额不变,因为中间被另一个线程又存了100.
9.synchronized解决线程安全问题线程出现安全问题的根本在于,多个线程对同一个资源进行操作时,一个线程操作数据时可能被另一个线程抢去执行权,等再回到该线程时,数据已经发生变化。造成数据错误。
如果能让某个线程执行某个操作时不被其他线程抢占cup资源,等到该操作执行完成后,才能被其他线程抢占,则能解决线程安全问题。
对上面存钱取钱案例进行改进:仅仅只是在存钱和取钱的两个方法上添加了synchronized关键字。
package demo02;
public class AccountTest2 {
public static void main(String[] args) {
Account2 account2=new Account2();
new Thread(()->{
for (int i=0;i<10;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
account2.getMoney(100);
}
}).start();
new Thread(()->{
for (int i=0;i<10;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
account2.setMoney(100);
}
}).start();
}
}
class Account2{
//账户余额
private int money=0;
//取钱
public synchronized void getMoney(int num){
if(money>=num){
money-=num;
System.out.println(Thread.currentThread().getName()+"取了"+num+",余额为:"+money);
}
}
//存钱
public synchronized void setMoney(int num){
money+=num;
System.out.println(Thread.currentThread().getName()+"存了"+num+",余额为:"+money);
}
}
执行结果:
synchronized关键字有两种用法,一是用在方法上,如上述代码,二是同步代码块,如:
public void getMoney(int num){
synchronized (this) {
if(money>=num){
money-=num;
System.out.println(Thread.currentThread().getName()+"取了"+num+",余额为:"+money);
}
}
}
同步代码块中的括号里的this叫做同步监视器(俗称锁),同步监视器可以用任意对象进行充当。只有当线程获得了锁后才能进入同步代码块中执行具体代码,如,当线程1执行存钱方法时,先获得锁,然后在执行具体的存钱代码,在这期间,如果被另一个线程抢去了cpu资源,另一个线程尝试执行取钱操作,但由于锁已经被线程1获得,所以线程2无法继续执行,并进入等待锁的阻塞状态,这时,只有当线程1重新获得cpu执行权,并继续执行完同步代码块中的内容后释放锁,线程2才有可能获得锁并继续执行。线程安全问题得以解决。
同步的局限性:当执行同步代码块时,只能有一个线程执行,相当于串行,效率较低。
注意事项:
多个线程必须要共用同一把锁。
同步的代码内容不能少,也不能过多,过多会影响效率,过少不安全。
synchronized作用在方法上和同步代码块中的异同:
10.线程死锁问题synchronized作用在普通方法上时锁为this,作用在静态方法上时锁为当前类的Class对象
synchronized作用在方法上时相当于这个方法的所有代码都是同步的。
synchronized获取锁和释放锁都是自动的。释放锁只有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁
形成原因:不同的线程分别占有对方需要的资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
死锁代码举例:
package demo02;
//死锁演示
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
StringBuffer s1=new StringBuffer();
StringBuffer s2=new StringBuffer();
//线程1
new Thread(() -> {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}).start();
//线程2
new Thread(()->{
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}).start();
}
}
执行结果:大概率出现死锁
分析:线程1执行到sleep时阻塞100ms,此时若线程2获得cpu执行权,则执行到sleep也会阻塞,当两个线程醒来后,发现线程1需要的锁s2在线程2手中,线程2需要的锁s1在线程1手中,则相互等待对方释放资源,导致死锁。
11.Lock方式解决线程安全问题Lock是一个接口,表示锁,它有很多实现类,这里使用ReentrantLock来举例。
package demo01;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//jdk5.0新增Lock锁
class Window implements Runnable{
private int ticket=100;
private Lock lock=new ReentrantLock();
@Override
public void run() {
while (true){
try {
//加锁
lock.lock();
if(ticket>0){
System.out.println(Thread.currentThread().getName()+":"+"出售第"+ticket+"张票");
ticket--;
}else {
break;
}
} finally {
//解锁
lock.unlock();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window window = new Window();
Thread t1 = new Thread(window);
Thread t2 = new Thread(window);
Thread t3 = new Thread(window);
t1.start();
t2.start();
t3.start();
}
}
Lock使用lock()方法和unlock方法来手动实现加锁和解锁,相比synchronized更灵活。
三种实现线程安全的方式推荐使用顺序:Lock=>synchronized代码块=>synchronized方法
12.线程通信需求:两个线程交替打印1-100 分析:线程的执行是随机的,如果向要线程按照我们想要的顺序去执行,则需要使用线程通信。
案例代码:
package demo01;
//线程通信案例;使用两个线程交替打印1-100
class Number implements Runnable{
private int num=1;
@Override
public void run() {
while (true){
synchronized (this) {
//唤醒其他线程
notify();
if(num<=100){
System.out.println(Thread.currentThread().getName()+":"+num);
num++;
try {
//线程等待阻塞,会释放锁
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
执行结果:按照需求交替打印。
分析:线程1打印后等待并释放锁,线程2执行,先唤醒线程1,但此时锁在线程2手中,线程1无法执行,线程2继续执行并打印,然后等待并释放锁,线程1执行,循环往复。



