一、基本概念
1.进程与线程
进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少一个线程。
线程:堆空间是共享的,栈空间是独立的,线程消耗的资源也比进程小,相互之间是可以影响的,又称之为轻型进程或者进程元。
线程的先后取决于JVM,程序员无法控制。JVM采用的是抢占式调度,没有分时调度,因此可能造成多线程执行结果的随机性。
2.创建线程
最传统的两种方式:
1.使用继承手段:继承Thread类
2.使用接口手段:实现Runnable接口
对于法1步骤:
①定义一个类A继承于java.lang.Thread类
②在A类中覆盖Thead类中的run方法
③我们在run方法中编写需要执行的操作—>run方法里的线程执行体。
④在我们的main方法(main线程)中创建一个线程对象并且启动。
A类 a =new A类();//创建一个线程
a.start();//启动线程。
注意:千万不用调用run方法。否则依然还是只有一个线程。
代码实例:
class MusicThread extends Thread
{
@Override
public void run() {
for(int i = 0; i < 50 ;i++) {
System.out.println("Music" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
for(int i = 0; i < 50 ; i++){
System.out.println("Game" + i);
if(i == 10) {
MusicThread mt = new MusicThread();
mt.start();
}
}
}
}
对于法2的步骤:
①定义一个类A实现于java.lang.Runnable接口
②在A类中覆盖Runnable接口中的run方法。
③在run方法中编写需要执行的操作
④在我们的main方法(main线程)中创建一个线程对象并且启动。
Thread a =new Thread(new A());//创建一个线程,注意:A类此时不是线程类
a.start();//启动线程。
此刻,Thread构造器需要一个Runnable对象/Runnable实现类的对象。
代码实例:
class MusicThread implements Runnable
{
@Override
public void run() {
for(int i = 0; i < 50 ;i++) {
System.out.println("Music" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
for(int i = 0; i < 50 ; i++){
System.out.println("Game" + i);
if(i == 10) {
Runnable target = new MusicThread();
Thread mt = new Thread(target);
mt.start();
}
}
}
}
对于两种方式:
继承方式:
①java中类是单继承的,如果继承了Thread了,该类就不能再有其他父类。
②从操作上来说,继承方式更为简单,操作也十分简单。
③从多线程共享同一个分析,继承方式不能做到
实现方式:
①java中类可以实现多接口,此时还可以继承其他类。
②从操作上分析,实现方式稍微复杂,获取线程的名字也比较复杂。
③从多线程共享同一个资源上讲,实现方式可做到。
建议多用实现方式创建线程。
3.线程的安全性问题
案例:
class Apple implements Runnable
{
private int num = 10;
@Override
public void run() {
for(int i = 0 ; i < 10 ; i++) {
if(num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"吃了"+ (num--) + "号苹果");
}
}
}
}
public class Demo {
public static void main(String[] args) {
Apple a = new Apple();
//三个线程共用同一个Apple对象
new Thread(a,"A").start();
new Thread(a,"B").start();
new Thread(a,"C").start();
}
}
在输出后,可以看到一个不正常的现象:
8,9号苹果都被吃了两次。C和A都拿到了编号为9的苹果,打印出来,还没来得及数num–的时候,A和C已经做了-1的操作,线程进入了睡眠,num还剩8个,B线程来了打印其值。此时A线程醒来做打印操作。
解决方案:A线程进入操作的时候,B和C都只能在外等着。保证打印和苹果减一操作必须同步完成。方式有3:
①同步代码块
②同步方法
③锁机制
法1:同步代码块
语法:
synchronized(同步锁)
{
//需要同步操作的代码块
}
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。Java程序运行使用任何对象作为同步监听对象,但是一般的,我们把当前并发访问的同步资源作为同步资源作为同步监听对象。
注意:在任何时候都只运行一个线程拥有同步锁。(谁拿到锁就进入代码块,其他的线程只能在外等着)
将上面的部分代码修改一下:
class Apple implements Runnable {
private int num = 10;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//同步代码块
synchronized (this) {//this表示属于多线程共享资源
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "吃了" + (num--) + "号苹果");
}
}
}
}
}
法2:同步方法
使用synchronized修饰的方法,就叫同步方法。保证了A线程执行该方法的时候,其他线程只能在外等着。
synchronized public void doWork()
{
//CODE
}
同步锁是谁?对于非static方法,同步锁就是this,对于static方法,我们使用当前方法所在类 的字节码对象(Apple.class)
但是,不要使用synchronized修饰run方法,因为修饰后某一线程就执行完了。就好比是多个线程出现了串行。正确操作是:把需要同步的代码块写在一个新的方法中,在run方法中去调用它。
class Apple implements Runnable {
private int num = 10;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
testMethod();
}
}
synchronized private void testMethod() {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "吃了" + (num--) + "号苹果");
}
}
}
synchronized的优劣性:
优点:保证了多线程并发访问时候的同步操作,避免了线程的安全性问题。
缺点:使用synchronized的方法/代码块性能会有所下降。
建议:尽量减少synchronized的作用域。
法3:锁机制
Lock机制提供了比synchronized更为广泛的锁定操作,同步代码块/同步方法具有的功能lock都有,更能体现面向对象。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Apple implements Runnable {
private int num = 10;
private final Lock lock = new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 10; i++) {
testMethod();
}
}
private void testMethod() {
//进入方法,立即加锁
lock.lock();//获取锁
try {
if (num > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "吃了" + (num--) + "号苹果");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();//释放锁
}
}
}